mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 00:11:19 +00:00
feat(ios): read-only workspace Files browser on the Agents surface (#100767)
* feat(ios): add read-only workspace Files browser to the Agents surface * fix(ios): keep nav bar visible on pushed workspace file routes * fix(ios): make workspace file navigation stack-safe * docs(ios): note workspace file browser * fix(ios): isolate workspace share exports * chore(i18n): refresh iOS Files inventory * refactor(ios): encode workspace requests before dispatch * chore(i18n): refresh iOS inventory after rebase
This commit is contained in:
parent
84f21bd9fc
commit
077e35f3b2
12 changed files with 578 additions and 66 deletions
|
|
@ -7355,7 +7355,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 64,
|
||||
"line": 72,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Destinations.swift",
|
||||
"source": "Skills",
|
||||
"surface": "apple",
|
||||
|
|
@ -7363,7 +7363,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 110,
|
||||
"line": 118,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Destinations.swift",
|
||||
"source": "Cron Jobs",
|
||||
"surface": "apple",
|
||||
|
|
@ -7371,7 +7371,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 140,
|
||||
"line": 148,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Destinations.swift",
|
||||
"source": "Usage",
|
||||
"surface": "apple",
|
||||
|
|
@ -7523,7 +7523,23 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 209,
|
||||
"line": 192,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Files",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.f48d33a93f53ae56"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 194,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Workspace files",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.b84aa382768fe819"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 216,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Dreaming",
|
||||
"surface": "apple",
|
||||
|
|
@ -7531,7 +7547,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 223,
|
||||
"line": 230,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Scheduled Work",
|
||||
"surface": "apple",
|
||||
|
|
@ -7539,7 +7555,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 302,
|
||||
"line": 309,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Selected agent",
|
||||
"surface": "apple",
|
||||
|
|
@ -7547,7 +7563,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 302,
|
||||
"line": 309,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Selects this agent",
|
||||
"surface": "apple",
|
||||
|
|
@ -7555,7 +7571,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 443,
|
||||
"line": 450,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Cron unavailable",
|
||||
"surface": "apple",
|
||||
|
|
@ -7563,7 +7579,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 443,
|
||||
"line": 450,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "No scheduled jobs",
|
||||
"surface": "apple",
|
||||
|
|
@ -7571,7 +7587,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 446,
|
||||
"line": 453,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Connect a gateway to load scheduled work.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7579,7 +7595,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 446,
|
||||
"line": 453,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "The gateway has no visible cron jobs.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7587,7 +7603,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 566,
|
||||
"line": 573,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Loading skill status.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7595,7 +7611,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 566,
|
||||
"line": 573,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Skill status is available from the gateway.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7603,7 +7619,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 588,
|
||||
"line": 595,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Instance presence is available.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7611,7 +7627,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 588,
|
||||
"line": 595,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Loading instance presence.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7619,7 +7635,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 607,
|
||||
"line": 614,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "\\(cronStatus.jobs)",
|
||||
"surface": "apple",
|
||||
|
|
@ -7627,7 +7643,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 613,
|
||||
"line": 620,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Cron status is available.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7635,7 +7651,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 613,
|
||||
"line": 620,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Loading cron status.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7643,7 +7659,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 618,
|
||||
"line": 625,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Scheduler disabled",
|
||||
"surface": "apple",
|
||||
|
|
@ -7651,7 +7667,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 618,
|
||||
"line": 625,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Scheduler enabled",
|
||||
"surface": "apple",
|
||||
|
|
@ -7659,7 +7675,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 643,
|
||||
"line": 650,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Loading recent usage.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7667,7 +7683,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 643,
|
||||
"line": 650,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Recent usage is available.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7675,7 +7691,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 662,
|
||||
"line": 669,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Background memory status is available.",
|
||||
"surface": "apple",
|
||||
|
|
@ -7683,7 +7699,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 662,
|
||||
"line": 669,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab+Overview.swift",
|
||||
"source": "Loading dreaming status.",
|
||||
"surface": "apple",
|
||||
|
|
@ -8067,7 +8083,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 58,
|
||||
"line": 59,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab.swift",
|
||||
"source": "Enabled",
|
||||
"surface": "apple",
|
||||
|
|
@ -8075,7 +8091,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 59,
|
||||
"line": 60,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab.swift",
|
||||
"source": "Off",
|
||||
"surface": "apple",
|
||||
|
|
@ -8083,7 +8099,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 60,
|
||||
"line": 61,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab.swift",
|
||||
"source": "Setup",
|
||||
"surface": "apple",
|
||||
|
|
@ -8091,7 +8107,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 61,
|
||||
"line": 62,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab.swift",
|
||||
"source": "Blocked",
|
||||
"surface": "apple",
|
||||
|
|
@ -8099,7 +8115,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 77,
|
||||
"line": 78,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab.swift",
|
||||
"source": "All",
|
||||
"surface": "apple",
|
||||
|
|
@ -8107,7 +8123,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 78,
|
||||
"line": 79,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab.swift",
|
||||
"source": "Online",
|
||||
"surface": "apple",
|
||||
|
|
@ -8115,12 +8131,68 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 79,
|
||||
"line": 80,
|
||||
"path": "apps/ios/Sources/Design/AgentProTab.swift",
|
||||
"source": "Ready",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.a97cdae7c8e13f0e"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 18,
|
||||
"path": "apps/ios/Sources/Design/AgentWorkspaceFilesScreen.swift",
|
||||
"source": "Files",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.9c4e8b82edf0b324"
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 62,
|
||||
"path": "apps/ios/Sources/Design/AgentWorkspaceFilesScreen.swift",
|
||||
"source": "This folder is empty.",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.ada58d7ce8866f71"
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 134,
|
||||
"path": "apps/ios/Sources/Design/AgentWorkspaceFilesScreen.swift",
|
||||
"source": "Load More",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.35512c4d46a2da28"
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 140,
|
||||
"path": "apps/ios/Sources/Design/AgentWorkspaceFilesScreen.swift",
|
||||
"source": "\\(self.entries.count) of \\(self.totalEntries)",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.e617cdacc3bce5ea"
|
||||
},
|
||||
{
|
||||
"kind": "ui-modifier",
|
||||
"line": 244,
|
||||
"path": "apps/ios/Sources/Design/AgentWorkspaceFilesScreen.swift",
|
||||
"source": "Share file",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.336a7eeb12d6e5f4"
|
||||
},
|
||||
{
|
||||
"kind": "ui-modifier",
|
||||
"line": 251,
|
||||
"path": "apps/ios/Sources/Design/AgentWorkspaceFilesScreen.swift",
|
||||
"source": "Could not share this file.",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.0ab324ff650d720c"
|
||||
},
|
||||
{
|
||||
"kind": "ui-call",
|
||||
"line": 255,
|
||||
"path": "apps/ios/Sources/Design/AgentWorkspaceFilesScreen.swift",
|
||||
"source": "OK",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.8218f673399060cc"
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 110,
|
||||
|
|
@ -9621,13 +9693,21 @@
|
|||
"kind": "ui-named-argument",
|
||||
"line": 165,
|
||||
"path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift",
|
||||
"source": "Files",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.3f2726ccd1acd987"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 170,
|
||||
"path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift",
|
||||
"source": "Dreaming",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.614c9b40919fb103"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 170,
|
||||
"line": 175,
|
||||
"path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift",
|
||||
"source": "Usage",
|
||||
"surface": "apple",
|
||||
|
|
@ -9635,7 +9715,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 175,
|
||||
"line": 180,
|
||||
"path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift",
|
||||
"source": "Cron Jobs",
|
||||
"surface": "apple",
|
||||
|
|
@ -9643,7 +9723,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 249,
|
||||
"line": 254,
|
||||
"path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift",
|
||||
"source": "Online",
|
||||
"surface": "apple",
|
||||
|
|
@ -9651,7 +9731,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 250,
|
||||
"line": 255,
|
||||
"path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift",
|
||||
"source": "Connecting",
|
||||
"surface": "apple",
|
||||
|
|
@ -9659,7 +9739,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 251,
|
||||
"line": 256,
|
||||
"path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift",
|
||||
"source": "Attention",
|
||||
"surface": "apple",
|
||||
|
|
@ -9667,7 +9747,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 252,
|
||||
"line": 257,
|
||||
"path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift",
|
||||
"source": "Offline",
|
||||
"surface": "apple",
|
||||
|
|
@ -13109,13 +13189,21 @@
|
|||
"kind": "ui-named-argument",
|
||||
"line": 487,
|
||||
"path": "apps/ios/Sources/RootTabs.swift",
|
||||
"source": "Files",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.d1a5c9b0c4acbfe2"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 494,
|
||||
"path": "apps/ios/Sources/RootTabs.swift",
|
||||
"source": "Dreaming",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.ad3c65353227b63e"
|
||||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 494,
|
||||
"line": 501,
|
||||
"path": "apps/ios/Sources/RootTabs.swift",
|
||||
"source": "Usage",
|
||||
"surface": "apple",
|
||||
|
|
@ -13123,7 +13211,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-named-argument",
|
||||
"line": 501,
|
||||
"line": 508,
|
||||
"path": "apps/ios/Sources/RootTabs.swift",
|
||||
"source": "Cron Jobs",
|
||||
"surface": "apple",
|
||||
|
|
@ -13131,7 +13219,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-modifier",
|
||||
"line": 634,
|
||||
"line": 641,
|
||||
"path": "apps/ios/Sources/RootTabs.swift",
|
||||
"source": "Hide Sidebar",
|
||||
"surface": "apple",
|
||||
|
|
@ -13139,7 +13227,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "ui-modifier",
|
||||
"line": 761,
|
||||
"line": 768,
|
||||
"path": "apps/ios/Sources/RootTabs.swift",
|
||||
"source": "Close canvas",
|
||||
"surface": "apple",
|
||||
|
|
@ -13147,7 +13235,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 1010,
|
||||
"line": 1017,
|
||||
"path": "apps/ios/Sources/RootTabs.swift",
|
||||
"source": "Gateway needs attention",
|
||||
"surface": "apple",
|
||||
|
|
@ -13155,7 +13243,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 1010,
|
||||
"line": 1017,
|
||||
"path": "apps/ios/Sources/RootTabs.swift",
|
||||
"source": "OpenClaw iOS",
|
||||
"surface": "apple",
|
||||
|
|
@ -13163,7 +13251,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 1046,
|
||||
"line": 1053,
|
||||
"path": "apps/ios/Sources/RootTabs.swift",
|
||||
"source": "Available",
|
||||
"surface": "apple",
|
||||
|
|
@ -13171,7 +13259,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 1046,
|
||||
"line": 1053,
|
||||
"path": "apps/ios/Sources/RootTabs.swift",
|
||||
"source": "Gateway default",
|
||||
"surface": "apple",
|
||||
|
|
@ -13179,7 +13267,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 63,
|
||||
"line": 64,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Chat",
|
||||
"surface": "apple",
|
||||
|
|
@ -13187,7 +13275,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 64,
|
||||
"line": 65,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Talk",
|
||||
"surface": "apple",
|
||||
|
|
@ -13195,7 +13283,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 65,
|
||||
"line": 66,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Overview",
|
||||
"surface": "apple",
|
||||
|
|
@ -13203,7 +13291,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 66,
|
||||
"line": 67,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Activity",
|
||||
"surface": "apple",
|
||||
|
|
@ -13211,7 +13299,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 67,
|
||||
"line": 68,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Agents",
|
||||
"surface": "apple",
|
||||
|
|
@ -13219,7 +13307,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 68,
|
||||
"line": 69,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Workboard",
|
||||
"surface": "apple",
|
||||
|
|
@ -13227,7 +13315,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 69,
|
||||
"line": 70,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Skill Workshop",
|
||||
"surface": "apple",
|
||||
|
|
@ -13235,7 +13323,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 70,
|
||||
"line": 71,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Instances",
|
||||
"surface": "apple",
|
||||
|
|
@ -13243,7 +13331,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 71,
|
||||
"line": 72,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Sessions",
|
||||
"surface": "apple",
|
||||
|
|
@ -13251,7 +13339,15 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 72,
|
||||
"line": 73,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Files",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.253cb2debeb51859"
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 74,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Dreaming",
|
||||
"surface": "apple",
|
||||
|
|
@ -13259,7 +13355,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 73,
|
||||
"line": 75,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Usage",
|
||||
"surface": "apple",
|
||||
|
|
@ -13267,7 +13363,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 74,
|
||||
"line": 76,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Cron Jobs",
|
||||
"surface": "apple",
|
||||
|
|
@ -13275,7 +13371,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 75,
|
||||
"line": 77,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Terminal",
|
||||
"surface": "apple",
|
||||
|
|
@ -13283,7 +13379,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 76,
|
||||
"line": 78,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Docs",
|
||||
"surface": "apple",
|
||||
|
|
@ -13291,7 +13387,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 77,
|
||||
"line": 79,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Settings",
|
||||
"surface": "apple",
|
||||
|
|
@ -13299,7 +13395,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 78,
|
||||
"line": 80,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Settings / Gateway",
|
||||
"surface": "apple",
|
||||
|
|
@ -13307,7 +13403,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 84,
|
||||
"line": 86,
|
||||
"path": "apps/ios/Sources/RootTabsNavigation.swift",
|
||||
"source": "Connection",
|
||||
"surface": "apple",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Added a read-only Files browser for agent workspaces with directory navigation, text and image previews, and share-sheet export.
|
||||
- Fixed Apple Watch connection setup so pairing and companion-install changes refresh immediately and cold launches wait for WatchConnectivity activation before sending.
|
||||
- Redesigned the Settings About screen with the animated mascot, app tagline, and Website/Docs/GitHub/Discord links.
|
||||
- Fixed startup aborts caused by inactive Voice Wake initializing the simulator audio pipeline.
|
||||
|
|
|
|||
|
|
@ -18,9 +18,17 @@ extension AgentProTab {
|
|||
self.usageDestination
|
||||
case .dreaming:
|
||||
self.dreamingDestination
|
||||
case .files:
|
||||
self.filesDestination
|
||||
}
|
||||
}
|
||||
|
||||
var filesDestination: some View {
|
||||
AgentWorkspaceFilesScreen(
|
||||
agentId: self.activeAgentID,
|
||||
headerLeadingAction: self.directHeaderLeadingAction(for: .files))
|
||||
}
|
||||
|
||||
var agentsDestination: some View {
|
||||
List {
|
||||
Section {
|
||||
|
|
|
|||
|
|
@ -187,6 +187,13 @@ extension AgentProTab {
|
|||
detail: self.usageDetail,
|
||||
color: self.gatewayConnected ? OpenClawBrand.accent : .secondary,
|
||||
route: .usage)
|
||||
self.metricTile(
|
||||
icon: "folder",
|
||||
title: "Files",
|
||||
value: self.activeAgentID,
|
||||
detail: "Workspace files",
|
||||
color: self.gatewayConnected ? OpenClawBrand.accent : .secondary,
|
||||
route: .files)
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ struct AgentProTab: View {
|
|||
case cron
|
||||
case usage
|
||||
case dreaming
|
||||
case files
|
||||
}
|
||||
|
||||
enum SkillStatusFilter: String, CaseIterable, Identifiable {
|
||||
|
|
|
|||
376
apps/ios/Sources/Design/AgentWorkspaceFilesScreen.swift
Normal file
376
apps/ios/Sources/Design/AgentWorkspaceFilesScreen.swift
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
import OpenClawChatUI
|
||||
import OpenClawKit
|
||||
import OpenClawProtocol
|
||||
import SwiftUI
|
||||
|
||||
/// Read-only browser for the selected agent's workspace, backed by the
|
||||
/// `agents.workspace.list` / `agents.workspace.get` gateway RPCs.
|
||||
struct AgentWorkspaceFilesScreen: View {
|
||||
let agentId: String
|
||||
let headerLeadingAction: OpenClawSidebarHeaderAction?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
OpenClawProBackground()
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let headerLeadingAction {
|
||||
OpenClawAdaptiveHeaderRow(
|
||||
title: "Files",
|
||||
subtitle: self.agentId,
|
||||
titleFont: OpenClawType.title3SemiBold,
|
||||
subtitleFont: OpenClawType.subheadMedium)
|
||||
{
|
||||
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
|
||||
} accessory: {
|
||||
EmptyView()
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
AgentWorkspaceDirectoryList(agentId: self.agentId, path: "")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Files")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
static func displayName(forPath path: String) -> String {
|
||||
path.split(separator: "/").last.map(String.init) ?? path
|
||||
}
|
||||
}
|
||||
|
||||
struct AgentWorkspaceDirectoryList: View {
|
||||
@Environment(NodeAppModel.self) var appModel
|
||||
let agentId: String
|
||||
let path: String
|
||||
|
||||
@State private var entries: [AgentsWorkspaceEntry] = []
|
||||
@State private var totalEntries = 0
|
||||
@State private var loading = false
|
||||
@State private var loadingMore = false
|
||||
@State private var errorText: String?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if let errorText {
|
||||
Section {
|
||||
Text(errorText)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
} else if self.entries.isEmpty, !self.loading {
|
||||
Section {
|
||||
Text("This folder is empty.")
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Section {
|
||||
ForEach(self.entries, id: \.path) { entry in
|
||||
self.entryRow(entry)
|
||||
}
|
||||
if self.entries.count < self.totalEntries {
|
||||
self.loadMoreRow
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.scrollContentBackground(.hidden)
|
||||
.font(OpenClawType.body)
|
||||
.overlay {
|
||||
if self.loading, self.entries.isEmpty {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
await self.reload()
|
||||
}
|
||||
.task(id: "\(self.agentId)|\(self.path)") {
|
||||
await self.reload()
|
||||
}
|
||||
}
|
||||
|
||||
private func entryRow(_ entry: AgentsWorkspaceEntry) -> some View {
|
||||
let isDirectory = self.isDirectory(entry)
|
||||
return NavigationLink {
|
||||
if isDirectory {
|
||||
ZStack {
|
||||
OpenClawProBackground()
|
||||
AgentWorkspaceDirectoryList(agentId: self.agentId, path: entry.path)
|
||||
}
|
||||
.navigationTitle(AgentWorkspaceFilesScreen.displayName(forPath: entry.path))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar(.visible, for: .navigationBar)
|
||||
} else {
|
||||
AgentWorkspaceFilePreview(agentId: self.agentId, path: entry.path)
|
||||
.toolbar(.visible, for: .navigationBar)
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: isDirectory ? "folder" : "doc.text")
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(isDirectory ? OpenClawBrand.accent : Color.secondary)
|
||||
.frame(width: 24)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(entry.name)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(1)
|
||||
if let detail = self.entryDetail(entry) {
|
||||
Text(detail)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var loadMoreRow: some View {
|
||||
Button {
|
||||
Task { await self.loadMore() }
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Load More")
|
||||
.font(OpenClawType.subheadMedium)
|
||||
Spacer()
|
||||
if self.loadingMore {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("\(self.entries.count) of \(self.totalEntries)")
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(self.loadingMore)
|
||||
}
|
||||
|
||||
private func isDirectory(_ entry: AgentsWorkspaceEntry) -> Bool {
|
||||
(entry.kind.value as? String) == "directory"
|
||||
}
|
||||
|
||||
private func entryDetail(_ entry: AgentsWorkspaceEntry) -> String? {
|
||||
var parts: [String] = []
|
||||
if let size = entry.size {
|
||||
parts.append(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file))
|
||||
}
|
||||
if let updatedAtMs = entry.updatedatms {
|
||||
let date = Date(timeIntervalSince1970: Double(updatedAtMs) / 1000)
|
||||
parts.append(date.formatted(.relative(presentation: .named, unitsStyle: .abbreviated)))
|
||||
}
|
||||
return parts.isEmpty ? nil : parts.joined(separator: " • ")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func reload() async {
|
||||
self.loading = true
|
||||
self.errorText = nil
|
||||
defer { self.loading = false }
|
||||
if let result = await self.fetchPage(offset: 0) {
|
||||
self.entries = result.entries
|
||||
self.totalEntries = result.totalentries
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadMore() async {
|
||||
guard !self.loadingMore else { return }
|
||||
self.loadingMore = true
|
||||
defer { self.loadingMore = false }
|
||||
if let result = await self.fetchPage(offset: self.entries.count) {
|
||||
let known = Set(self.entries.map(\.path))
|
||||
self.entries.append(contentsOf: result.entries.filter { !known.contains($0.path) })
|
||||
self.totalEntries = result.totalentries
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func fetchPage(offset: Int) async -> AgentsWorkspaceListResult? {
|
||||
do {
|
||||
let params = AgentsWorkspaceListParams(
|
||||
agentid: self.agentId,
|
||||
path: self.path.isEmpty ? nil : self.path,
|
||||
offset: offset == 0 ? nil : offset,
|
||||
limit: nil)
|
||||
let paramsJSON = try Self.encodeParams(params)
|
||||
let data = try await self.appModel.operatorSession.request(
|
||||
method: "agents.workspace.list",
|
||||
paramsJSON: paramsJSON,
|
||||
timeoutSeconds: 12)
|
||||
return try JSONDecoder().decode(AgentsWorkspaceListResult.self, from: data)
|
||||
} catch {
|
||||
self.errorText = "Could not load this folder."
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func encodeParams(_ params: some Encodable) throws -> String {
|
||||
let data = try JSONEncoder().encode(params)
|
||||
return String(data: data, encoding: .utf8) ?? "{}"
|
||||
}
|
||||
}
|
||||
|
||||
struct AgentWorkspaceFilePreview: View {
|
||||
@Environment(NodeAppModel.self) var appModel
|
||||
let agentId: String
|
||||
let path: String
|
||||
|
||||
private struct ShareItem: Identifiable {
|
||||
let id = UUID()
|
||||
let fileURL: URL
|
||||
}
|
||||
|
||||
@State private var file: AgentsWorkspaceFile?
|
||||
@State private var loading = false
|
||||
@State private var errorText: String?
|
||||
@State private var shareItem: ShareItem?
|
||||
@State private var showsShareError = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
OpenClawProBackground()
|
||||
self.content
|
||||
}
|
||||
.navigationTitle(AgentWorkspaceFilesScreen.displayName(forPath: self.path))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
self.share()
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.accessibilityLabel("Share file")
|
||||
.disabled(self.file == nil)
|
||||
}
|
||||
}
|
||||
.sheet(item: self.$shareItem) { item in
|
||||
ChatTranscriptShareSheet(fileURL: item.fileURL)
|
||||
}
|
||||
.alert("Could not share this file.", isPresented: self.$showsShareError) {
|
||||
Button {
|
||||
self.showsShareError = false
|
||||
} label: {
|
||||
Text("OK")
|
||||
.font(OpenClawType.subheadMedium)
|
||||
}
|
||||
}
|
||||
.task(id: "\(self.agentId)|\(self.path)") {
|
||||
await self.load()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
if let file {
|
||||
self.preview(for: file)
|
||||
} else if self.loading {
|
||||
ProgressView()
|
||||
} else if let errorText {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "doc.questionmark")
|
||||
.font(OpenClawType.title3SemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(errorText)
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(OpenClawProMetric.pagePadding)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func preview(for file: AgentsWorkspaceFile) -> some View {
|
||||
if self.isImage(file), let image = self.decodedImage(file) {
|
||||
ScrollView([.horizontal, .vertical]) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(OpenClawProMetric.pagePadding)
|
||||
}
|
||||
} else {
|
||||
ScrollView([.horizontal, .vertical]) {
|
||||
Text(ChatCodeHighlightCache.highlighted(
|
||||
code: file.content,
|
||||
languageId: self.languageId))
|
||||
.font(OpenClawType.monoSmall)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(OpenClawProMetric.pagePadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var fileExtension: String {
|
||||
(self.path as NSString).pathExtension.lowercased()
|
||||
}
|
||||
|
||||
/// The highlighter's language ids line up with common file extensions;
|
||||
/// unknown ids fall back to plain text rendering.
|
||||
private var languageId: String? {
|
||||
self.fileExtension.isEmpty ? nil : self.fileExtension
|
||||
}
|
||||
|
||||
private func isImage(_ file: AgentsWorkspaceFile) -> Bool {
|
||||
(file.encoding.value as? String) == "base64" && file.mimetype.hasPrefix("image/")
|
||||
}
|
||||
|
||||
private func decodedImage(_ file: AgentsWorkspaceFile) -> UIImage? {
|
||||
guard let data = Data(base64Encoded: file.content) else { return nil }
|
||||
return UIImage(data: data)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func load() async {
|
||||
self.loading = true
|
||||
self.file = nil
|
||||
self.errorText = nil
|
||||
defer { self.loading = false }
|
||||
do {
|
||||
let params = AgentsWorkspaceGetParams(agentid: self.agentId, path: self.path)
|
||||
let paramsJSON = try AgentWorkspaceDirectoryList.encodeParams(params)
|
||||
let data = try await self.appModel.operatorSession.request(
|
||||
method: "agents.workspace.get",
|
||||
paramsJSON: paramsJSON,
|
||||
timeoutSeconds: 20)
|
||||
self.file = try JSONDecoder().decode(AgentsWorkspaceGetResult.self, from: data).file
|
||||
} catch {
|
||||
self.errorText = "This file cannot be previewed. It may be binary or too large."
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors the chat transcript export flow: write a temp copy, then hand
|
||||
/// it to the system share sheet.
|
||||
private func share() {
|
||||
guard let file else { return }
|
||||
let safeName = (file.name as NSString).lastPathComponent
|
||||
guard !safeName.isEmpty, safeName != ".", safeName != ".." else {
|
||||
self.showsShareError = true
|
||||
return
|
||||
}
|
||||
// Each share gets stable bytes at a unique URL; a later same-basename
|
||||
// export must not replace the item already handed to an extension.
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("OpenClawWorkspaceFiles", isDirectory: true)
|
||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
let fileURL = directory.appendingPathComponent(safeName, isDirectory: false)
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
if self.isImage(file) {
|
||||
guard let data = Data(base64Encoded: file.content) else {
|
||||
self.showsShareError = true
|
||||
return
|
||||
}
|
||||
try data.write(to: fileURL, options: .atomic)
|
||||
} else {
|
||||
try file.content.write(to: fileURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
self.shareItem = ShareItem(fileURL: fileURL)
|
||||
} catch {
|
||||
self.showsShareError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -159,6 +159,11 @@ struct RootTabsPhoneControlHub: View {
|
|||
CommandSessionsScreen(
|
||||
usesNativeNavigationChrome: true,
|
||||
openChat: { self.openChatFromControlDetail(.sessions) })
|
||||
case .files:
|
||||
AgentProTab(
|
||||
directRoute: .files,
|
||||
headerTitle: "Files",
|
||||
openSettings: { self.openGatewayDetail() })
|
||||
case .dreaming:
|
||||
AgentProTab(
|
||||
directRoute: .dreaming,
|
||||
|
|
|
|||
|
|
@ -480,6 +480,13 @@ struct RootTabs: View {
|
|||
CommandSessionsScreen(
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
openChat: { self.selectSidebarDestination(.chat) })
|
||||
case .files:
|
||||
AgentProTab(
|
||||
directRoute: .files,
|
||||
headerLeadingAction: self.sidebarHeaderLeadingAction,
|
||||
headerTitle: "Files",
|
||||
openSettings: { self.selectSidebarDestination(.gateway) })
|
||||
.id(self.selectedSidebarDestination.id)
|
||||
case .dreaming:
|
||||
AgentProTab(
|
||||
directRoute: .dreaming,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ extension RootTabs {
|
|||
case skillWorkshop
|
||||
case instances
|
||||
case sessions
|
||||
case files
|
||||
case dreaming
|
||||
case usage
|
||||
case cron
|
||||
|
|
@ -69,6 +70,7 @@ extension RootTabs {
|
|||
case .skillWorkshop: "Skill Workshop"
|
||||
case .instances: "Instances"
|
||||
case .sessions: "Sessions"
|
||||
case .files: "Files"
|
||||
case .dreaming: "Dreaming"
|
||||
case .usage: "Usage"
|
||||
case .cron: "Cron Jobs"
|
||||
|
|
@ -97,6 +99,7 @@ extension RootTabs {
|
|||
case .skillWorkshop: "hammer"
|
||||
case .instances: "dot.radiowaves.left.and.right"
|
||||
case .sessions: "doc.text"
|
||||
case .files: "folder.fill"
|
||||
case .dreaming: "moon.stars"
|
||||
case .usage: "chart.bar.xaxis"
|
||||
case .cron: "timer"
|
||||
|
|
@ -117,7 +120,8 @@ extension RootTabs {
|
|||
.agent
|
||||
case .settings, .gateway:
|
||||
.settings
|
||||
case .overview, .activity, .workboard, .skillWorkshop, .instances, .sessions, .dreaming,
|
||||
case .overview, .activity, .workboard, .skillWorkshop, .instances, .sessions, .files,
|
||||
.dreaming,
|
||||
.usage,
|
||||
.cron, .terminal, .docs:
|
||||
.control
|
||||
|
|
@ -129,6 +133,7 @@ extension RootTabs {
|
|||
case .gateway:
|
||||
.gateway
|
||||
case .chat, .talk, .overview, .activity, .agents, .workboard, .skillWorkshop, .instances, .sessions,
|
||||
.files,
|
||||
.dreaming,
|
||||
.usage, .cron, .terminal, .settings, .docs:
|
||||
nil
|
||||
|
|
@ -199,7 +204,8 @@ extension RootTabs {
|
|||
switch destination {
|
||||
case .chat, .talk, .agents, .gateway, .settings:
|
||||
true
|
||||
case .overview, .activity, .workboard, .skillWorkshop, .instances, .sessions, .dreaming,
|
||||
case .overview, .activity, .workboard, .skillWorkshop, .instances, .sessions, .files,
|
||||
.dreaming,
|
||||
.usage,
|
||||
.cron, .terminal, .docs:
|
||||
false
|
||||
|
|
@ -283,6 +289,7 @@ extension RootTabs {
|
|||
.skillWorkshop,
|
||||
.instances,
|
||||
.sessions,
|
||||
.files,
|
||||
.dreaming,
|
||||
.usage,
|
||||
.cron,
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ struct RootTabsPresentationTests {
|
|||
.skillWorkshop,
|
||||
.instances,
|
||||
.sessions,
|
||||
.files,
|
||||
.dreaming,
|
||||
.usage,
|
||||
.cron,
|
||||
|
|
@ -93,6 +94,7 @@ struct RootTabsPresentationTests {
|
|||
"skillWorkshop",
|
||||
"instances",
|
||||
"sessions",
|
||||
"files",
|
||||
"dreaming",
|
||||
"usage",
|
||||
"cron",
|
||||
|
|
|
|||
|
|
@ -354,14 +354,15 @@ enum ChatCodeHighlighter {
|
|||
|
||||
/// Content-keyed memo so streaming re-renders reuse finished highlight work
|
||||
/// instead of re-tokenizing every completed block on each delta tick.
|
||||
/// Public so app file previews reuse the chat renderer's highlighting.
|
||||
@MainActor
|
||||
enum ChatCodeHighlightCache {
|
||||
public enum ChatCodeHighlightCache {
|
||||
private static var cache: [String: AttributedString] = [:]
|
||||
// Bounded: wholesale reset past the cap keeps long sessions flat; a miss
|
||||
// just re-tokenizes one visible block, which is cheap.
|
||||
private static let capacity = 160
|
||||
|
||||
static func highlighted(code: String, languageId: String?) -> AttributedString {
|
||||
public static func highlighted(code: String, languageId: String?) -> AttributedString {
|
||||
guard ChatCodeHighlighter.language(for: languageId) != nil,
|
||||
ChatCodeHighlighter.isWithinHighlightLimits(code)
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ Availability: iPhone app builds are distributed through Apple channels when enab
|
|||
- Connects to a Gateway over WebSocket (LAN or tailnet).
|
||||
- Exposes node capabilities: Canvas, Screen snapshot, Camera capture, Location, Talk mode, Voice wake.
|
||||
- Receives `node.invoke` commands and reports node status events.
|
||||
- Browses the selected agent's workspace read-only from the Agents surface (Files): directory drill-down, syntax-highlighted text previews, image previews, and share-sheet export. No write operations; previews are size-capped by the gateway.
|
||||
- Keeps a small read-only offline cache of recent chat sessions and transcripts per paired gateway: cold opens paint the last known transcript immediately and refresh once the gateway responds, recent chats stay browsable while disconnected, and reset/forget purges the protected local cache.
|
||||
- Queues text messages sent while disconnected in a durable per-gateway outbox (up to 50): queued bubbles show in the transcript, flush in order on reconnect with idempotency keys so nothing sends twice, retry with backoff before surfacing as "Not sent" with retry/delete in the message context menu, and expire instead of sending after 48 hours offline; reset/forget clears the queue with the cache.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue