Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c1f75d0
feat(db): add activityLogs table for audit trail
2witstudios Dec 18, 2025
a513f10
feat(lib): add activity logger service
2witstudios Dec 18, 2025
6c6090f
feat(api): add /api/activities endpoint
2witstudios Dec 18, 2025
f612f95
feat(ui): replace Settings tab with Activity tab in sidebar
2witstudios Dec 18, 2025
ccc2cc0
refactor(store): update SidebarTab type from 'settings' to 'activity'
2witstudios Dec 18, 2025
d48f3f6
feat(api): inject activity logging into page CRUD routes
2witstudios Dec 18, 2025
8a4b59b
feat(api): inject activity logging into permission routes
2witstudios Dec 18, 2025
2c43d87
fix(api): handle null values in activities query params
2witstudios Dec 18, 2025
cb8eccc
feat(ai): add activity logging to all AI tool operations
2witstudios Dec 18, 2025
534a814
fix: address PR review comments
2witstudios Dec 19, 2025
e7d2b58
test: fix mocks for activity logging functions
2witstudios Dec 19, 2025
b893e8c
fix: prefix unused provider param with underscore
2witstudios Dec 19, 2025
b538ebf
Rewire iOS app after web refactor (#97)
2witstudios Dec 19, 2025
60e4275
fix(stores): restore missing page header and clean up dead navigation…
2witstudios Dec 19, 2025
ca507e7
refactor: extract URL state and agent conversation helpers (#101)
2witstudios Dec 19, 2025
f028da6
feat(db): add activityLogs table for audit trail
2witstudios Dec 18, 2025
1493c4f
feat(lib): add activity logger service
2witstudios Dec 18, 2025
1c330e8
feat(api): add /api/activities endpoint
2witstudios Dec 18, 2025
0765d17
feat(ui): replace Settings tab with Activity tab in sidebar
2witstudios Dec 18, 2025
d9f3edd
refactor(store): update SidebarTab type from 'settings' to 'activity'
2witstudios Dec 18, 2025
805c5d6
feat(api): inject activity logging into page CRUD routes
2witstudios Dec 18, 2025
123f16c
feat(api): inject activity logging into permission routes
2witstudios Dec 18, 2025
27db693
fix(api): handle null values in activities query params
2witstudios Dec 18, 2025
06f6583
feat(ai): add activity logging to all AI tool operations
2witstudios Dec 18, 2025
2d40943
fix: address PR review comments
2witstudios Dec 19, 2025
96d69c5
test: fix mocks for activity logging functions
2witstudios Dec 19, 2025
c62f191
fix: prefix unused provider param with underscore
2witstudios Dec 19, 2025
ead6eb0
Merge branch 'feat/activity-monitoring-system' of github.com:2witstud…
2witstudios Dec 19, 2025
33feb16
refactor: replace ORM chain mocks with repository seams
2witstudios Dec 19, 2025
f53eac3
fix(test): add missing isEditingActive export to usePageTree test mock
2witstudios Dec 19, 2025
5ac4aae
fix: remove unused db imports from page-write-tools
2witstudios Dec 19, 2025
368f706
fix(test): add missing getActorInfo and logPageActivity mocks
2witstudios Dec 19, 2025
41dee27
refactor: implement fire-and-forget activity logging pattern
2witstudios Dec 19, 2025
6efda0e
fix: use null instead of undefined for parentId when moving to root
2witstudios Dec 19, 2025
b7f7f1a
fix: prefix unused mockLogDriveActivity with underscore
2witstudios Dec 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions apps/ios/PageSpace/Core/Models/Conversation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ struct Conversation: Identifiable, Codable {
}
}

struct ConversationListResponse: Codable {
let conversations: [Conversation]
let total: Int
}

struct ConversationMessagesResponse: Codable {
let messages: [Message]
let pagination: Pagination?
Expand Down
56 changes: 56 additions & 0 deletions apps/ios/PageSpace/Core/Models/Page.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,59 @@ struct PageAIMessageRequest: Codable {
struct PageAIMessagesResponse: Codable {
let messages: [Message]
}

// MARK: - Multi-Drive Agents API Response

struct MultiDriveAgentsResponse: Codable {
let success: Bool
let totalCount: Int
let driveCount: Int
let summary: String?
let agentsByDrive: [DriveAgentGroup]?
let agents: [AgentSummary]? // When groupByDrive=false
}

struct DriveAgentGroup: Codable {
let driveId: String
let driveName: String
let driveSlug: String
let agentCount: Int
let agents: [AgentSummary]
}

struct AgentSummary: Codable {
let id: String
let title: String?
let parentId: String?
let position: Int?
let aiProvider: String?
let aiModel: String?
let hasWelcomeMessage: Bool?
let createdAt: Date?
let updatedAt: Date?
let driveId: String?
let driveName: String?
let driveSlug: String?
let hasSystemPrompt: Bool?
let systemPromptPreview: String?
let enabledToolsCount: Int?

Comment on lines +216 to +232

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Type inconsistency: position field should be Double.

The position field is declared as Int? here, but the Page struct (line 11) uses Double to support fractional positions from drag-and-drop reordering. This type mismatch could cause precision loss and ordering issues if agent positions need to be compared or synchronized with page positions.

🔎 Suggested fix
 struct AgentSummary: Codable {
     let id: String
     let title: String?
     let parentId: String?
-    let position: Int?
+    let position: Double?
     let aiProvider: String?
     let aiModel: String?
🤖 Prompt for AI Agents
In apps/ios/PageSpace/Core/Models/Page.swift around lines 216 to 232, the
AgentSummary struct declares position as Int? which is inconsistent with Page
(which uses Double) and can cause precision loss; change the
AgentSummary.position type to Double? and update any decoding/encoding code if
custom coding exists so JSON numbers (including fractional values) are handled
correctly, then run unit/encode-decode checks to ensure no runtime decoding
failures.

/// Convert to Agent model for UI display
func toAgent() -> Agent {
Agent(
id: "page_\(id)",
type: .pageAI,
title: title ?? "Untitled Agent",
subtitle: driveName.map { "\($0)" },
icon: "bubble.left.and.text.bubble.right",
driveId: driveId,
driveName: driveName,
pageId: id,
pagePath: nil,
aiProvider: aiProvider,
aiModel: aiModel,
systemPrompt: nil, // Not included in summary
enabledTools: nil // Not included in summary
)
}
}
5 changes: 4 additions & 1 deletion apps/ios/PageSpace/Core/Models/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ struct ProviderStatuses: Codable {
let ollama: ProviderStatus?
let lmstudio: ProviderStatus?
let glm: ProviderStatus?
let minimax: ProviderStatus?

enum CodingKeys: String, CodingKey {
case pagespace, openrouter, google, openai, anthropic, xai, ollama, lmstudio, glm
case pagespace, openrouter, google, openai, anthropic, xai, ollama, lmstudio, glm, minimax
}
}

Expand Down Expand Up @@ -143,6 +144,8 @@ extension AISettings {
return providers.lmstudio?.isConfigured ?? false
case "glm":
return providers.glm?.isConfigured ?? false
case "minimax":
return providers.minimax?.isConfigured ?? false
default:
return false
}
Expand Down
7 changes: 7 additions & 0 deletions apps/ios/PageSpace/Core/Networking/APIEndpoints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,11 @@ enum APIEndpoints {

// MARK: - Search
static let search = "/api/search"

// MARK: - Page Agents
static let multiDriveAgents = "/api/ai/page-agents/multi-drive"

static func driveAgents(driveId: String) -> String {
"/api/drives/\(driveId)/agents"
}
}
77 changes: 29 additions & 48 deletions apps/ios/PageSpace/Core/Services/AgentService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,75 +22,56 @@ class AgentService: ObservableObject {
error = nil

var allAgents: [Agent] = []
var globalAgent: Agent?
var errorMessages: [String] = []

// 1. Create Global Assistant agent (no conversation ID needed)
globalAgent = Agent(
// 1. Create Global Assistant agent (always available)
let globalAgent = Agent(
id: "global_default",
type: .global,
title: "Global Assistant",
subtitle: "Your personal AI assistant",
icon: "brain.head.profile"
)
allAgents.append(globalAgent!)
allAgents.append(globalAgent)
print("✅ Global Assistant agent created")

// 2. Load all drives and their AI chat pages
// 2. Load all page agents via dedicated multi-drive endpoint
do {
let drives: [Drive] = try await apiClient.request(
endpoint: APIEndpoints.drives,
let response: MultiDriveAgentsResponse = try await apiClient.request(
endpoint: APIEndpoints.multiDriveAgents,
method: .GET
)
print("✅ Loaded \(drives.count) drives")

// For each drive, load pages and filter AI_CHAT pages
for drive in drives {
do {
// Backend returns tree array directly (not wrapped)
let pageTree: [Page] = try await apiClient.request(
endpoint: APIEndpoints.drivePages(driveId: drive.id),
method: .GET
)

// Flatten tree structure to get all pages
let allPages = flattenPageTree(pageTree)
print("✅ Drive '\(drive.name)': Loaded \(allPages.count) pages")

// Filter to AI_CHAT pages only
let aiChatPages = allPages.filter { $0.type == .aiChat }
print(" └─ Found \(aiChatPages.count) AI chat pages")

// Create agents from AI chat pages
for page in aiChatPages {
let agent = Agent.fromPage(page, drive: drive)
allAgents.append(agent)
print("✅ Loaded agents from \(response.driveCount) drives")

// Convert AgentSummary to Agent models
if let agentGroups = response.agentsByDrive {
for group in agentGroups {
print(" └─ Drive '\(group.driveName)': \(group.agentCount) agents")
for agentSummary in group.agents {
allAgents.append(agentSummary.toAgent())
}
} catch {
// Log error but continue with other drives
let errorMsg = "Failed to load pages for drive '\(drive.name)': \(error.localizedDescription)"
errorMessages.append(errorMsg)
print("❌ \(errorMsg)")
}
} else if let flatAgents = response.agents {
// Flat list response (groupByDrive=false)
for agentSummary in flatAgents {
allAgents.append(agentSummary.toAgent())
}
}

print("📊 Total page agents loaded: \(response.totalCount)")
} catch {
// If we can't load drives, that's a critical error
errorMessages.append("Failed to load drives: \(error.localizedDescription)")
print("❌ Drives error: \(error)")
// Log error but don't fail completely - we still have global agent
let errorMsg = "Failed to load page agents: \(error.localizedDescription)"
self.error = errorMsg
print("❌ \(errorMsg)")
}

// Update published property with whatever we managed to load
// Update published property
agents = allAgents
print("📊 Total agents loaded: \(allAgents.count)")
print("📊 Total agents available: \(allAgents.count)")

// Set default selected agent to global if none selected
if selectedAgent == nil, let defaultAgent = globalAgent {
selectedAgent = defaultAgent
}

// Set error message if any errors occurred (but don't fail completely)
if !errorMessages.isEmpty {
self.error = errorMessages.joined(separator: "\n")
if selectedAgent == nil {
selectedAgent = globalAgent
}

isLoading = false
Expand Down
Loading