@agent-native/core 0.146.8 → 0.147.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +18 -6
  3. package/corpus/templates/analytics/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  4. package/corpus/templates/analytics/app/routes/status.$slug.tsx +4 -2
  5. package/corpus/templates/analytics/scripts/first-party-analytics-bigquery-backfill.ts +56 -6
  6. package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +18 -6
  7. package/corpus/templates/assets/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  8. package/corpus/templates/brain/.agents/skills/adding-a-feature/SKILL.md +9 -0
  9. package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +18 -6
  10. package/corpus/templates/brain/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  11. package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +18 -6
  12. package/corpus/templates/calendar/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  13. package/corpus/templates/chat/.agents/skills/adding-a-feature/SKILL.md +9 -0
  14. package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +18 -6
  15. package/corpus/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  16. package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +18 -6
  17. package/corpus/templates/clips/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  18. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +16 -4
  19. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +14 -5
  20. package/corpus/templates/clips/package.json +1 -0
  21. package/corpus/templates/clips/react-router.config.ts +7 -0
  22. package/corpus/templates/clips/server/plugins/auth.ts +6 -1
  23. package/corpus/templates/clips/shared/prerendered-public-paths.ts +21 -0
  24. package/corpus/templates/clips/shared/share-meta.ts +8 -3
  25. package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +18 -6
  26. package/corpus/templates/content/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  27. package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +18 -6
  28. package/corpus/templates/crm/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  29. package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +18 -6
  30. package/corpus/templates/design/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  31. package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +18 -6
  32. package/corpus/templates/dispatch/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  33. package/corpus/templates/dispatch/app/chat-first-pages.ts +747 -0
  34. package/corpus/templates/dispatch/app/i18n-data.ts +181 -0
  35. package/corpus/templates/dispatch/app/routes/settings.tsx +62 -1
  36. package/corpus/templates/dispatch/changelog/2026-08-08-chat-first-navigation-keeps-conversations-central-while-open.md +6 -0
  37. package/corpus/templates/factory/.agents/skills/adding-a-feature/SKILL.md +9 -0
  38. package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +18 -6
  39. package/corpus/templates/factory/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  40. package/corpus/templates/factory/app/routes/factory.tsx +108 -76
  41. package/corpus/templates/factory/changelog/2026-08-08-scheduler-health-moves-to-settings.md +6 -0
  42. package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +18 -6
  43. package/corpus/templates/forms/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  44. package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +18 -6
  45. package/corpus/templates/macros/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  46. package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +18 -6
  47. package/corpus/templates/mail/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  48. package/corpus/templates/plan/.agents/skills/adding-a-feature/SKILL.md +9 -0
  49. package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +18 -6
  50. package/corpus/templates/plan/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  51. package/corpus/templates/plan/app/lib/plan-document-title.ts +3 -1
  52. package/corpus/templates/plan/app/root.tsx +1 -0
  53. package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +18 -6
  54. package/corpus/templates/slides/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  55. package/corpus/templates/tasks/.agents/skills/adding-a-feature/SKILL.md +9 -0
  56. package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +18 -6
  57. package/corpus/templates/tasks/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  58. package/dist/agent/engine/builder-engine.d.ts +11 -1
  59. package/dist/agent/engine/builder-engine.js +11 -4
  60. package/dist/chat-threads/store.d.ts +2 -0
  61. package/dist/chat-threads/store.js +1 -1
  62. package/dist/cli/agent-plugin.d.ts +5 -2
  63. package/dist/cli/agent-plugin.js +17 -9
  64. package/dist/cli/code-agent-executor.js +25 -5
  65. package/dist/cli/code-agent-mcp-config.d.ts +21 -0
  66. package/dist/cli/code-agent-mcp-config.js +103 -0
  67. package/dist/client/NewWorkspaceAppFlow.js +4 -1
  68. package/dist/client/agent-chat/index.d.ts +4 -1
  69. package/dist/client/agent-chat/index.js +4 -1
  70. package/dist/client/agent-page/AgentTabsPage.d.ts +1 -0
  71. package/dist/client/agent-page/AgentTabsPage.js +4 -3
  72. package/dist/client/app-providers.d.ts +3 -1
  73. package/dist/client/app-providers.js +50 -5
  74. package/dist/client/chat/runtime.js +63 -0
  75. package/dist/client/chat-first/agents-pane.d.ts +2 -0
  76. package/dist/client/chat-first/agents-pane.js +6 -0
  77. package/dist/client/chat-first/app-pane.d.ts +2 -0
  78. package/dist/client/chat-first/app-pane.js +6 -0
  79. package/dist/client/chat-first/apps-rail.d.ts +2 -0
  80. package/dist/client/chat-first/apps-rail.js +103 -0
  81. package/dist/client/chat-first/browser-pane.d.ts +2 -0
  82. package/dist/client/chat-first/browser-pane.js +64 -0
  83. package/dist/client/chat-first/chat-history.d.ts +12 -0
  84. package/dist/client/chat-first/chat-history.js +10 -0
  85. package/dist/client/chat-first/copy.d.ts +2 -0
  86. package/dist/client/chat-first/copy.js +117 -0
  87. package/dist/client/chat-first/index.d.ts +13 -0
  88. package/dist/client/chat-first/index.js +12 -0
  89. package/dist/client/chat-first/primary-nav.d.ts +8 -0
  90. package/dist/client/chat-first/primary-nav.js +6 -0
  91. package/dist/client/chat-first/session-watch-pane.d.ts +2 -0
  92. package/dist/client/chat-first/session-watch-pane.js +9 -0
  93. package/dist/client/chat-first/surface-panel.d.ts +9 -0
  94. package/dist/client/chat-first/surface-panel.js +9 -0
  95. package/dist/client/chat-first/surface-tabs.d.ts +9 -0
  96. package/dist/client/chat-first/surface-tabs.js +74 -0
  97. package/dist/client/chat-first/types.d.ts +74 -0
  98. package/dist/client/chat-first/types.js +1 -0
  99. package/dist/client/chat-first-agent-activity.d.ts +11 -0
  100. package/dist/client/chat-first-agent-activity.js +51 -0
  101. package/dist/client/chat-first-surface-panel-toggle.d.ts +5 -0
  102. package/dist/client/chat-first-surface-panel-toggle.js +6 -0
  103. package/dist/client/chat-first.d.ts +208 -0
  104. package/dist/client/chat-first.js +816 -0
  105. package/dist/client/extensions/ExtensionViewer.js +8 -1
  106. package/dist/client/resources/McpIntegrationDialog.js +3 -2
  107. package/dist/client/resources/McpServerDetail.js +6 -11
  108. package/dist/client/resources/index.d.ts +1 -0
  109. package/dist/client/resources/index.js +1 -0
  110. package/dist/client/resources/use-mcp-servers.d.ts +20 -0
  111. package/dist/client/resources/use-mcp-servers.js +82 -46
  112. package/dist/client/settings/index.d.ts +1 -0
  113. package/dist/client/settings/index.js +1 -0
  114. package/dist/collab/awareness.d.ts +2 -2
  115. package/dist/collab/routes.d.ts +1 -1
  116. package/dist/collab/struct-routes.d.ts +1 -1
  117. package/dist/jobs/background-automation-runner.js +19 -2
  118. package/dist/jobs/scheduler-health.d.ts +20 -0
  119. package/dist/jobs/scheduler-health.js +86 -1
  120. package/dist/jobs/scheduler.js +38 -1
  121. package/dist/mcp-client/routes.d.ts +3 -0
  122. package/dist/mcp-client/routes.js +14 -0
  123. package/dist/notifications/routes.d.ts +1 -1
  124. package/dist/observability/routes.d.ts +5 -5
  125. package/dist/progress/routes.d.ts +1 -1
  126. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  127. package/dist/resources/handlers.d.ts +1 -1
  128. package/dist/secrets/routes.d.ts +3 -3
  129. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  130. package/dist/server/agents-bundle.d.ts +2 -0
  131. package/dist/server/agents-bundle.js +31 -2
  132. package/dist/server/credential-provider.d.ts +8 -2
  133. package/dist/server/credential-provider.js +13 -9
  134. package/dist/server/realtime-token.d.ts +1 -1
  135. package/dist/server/transcribe-voice.d.ts +1 -1
  136. package/dist/shared/chat-first-app-creation.d.ts +19 -0
  137. package/dist/shared/chat-first-app-creation.js +75 -0
  138. package/dist/shared/document-title.d.ts +7 -0
  139. package/dist/shared/document-title.js +29 -0
  140. package/dist/shared/index.d.ts +2 -0
  141. package/dist/shared/index.js +2 -0
  142. package/dist/styles/chat-history-list.css +16 -0
  143. package/dist/templates/chat/.agents/skills/adding-a-feature/SKILL.md +9 -0
  144. package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +18 -6
  145. package/dist/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  146. package/dist/templates/default/.agents/skills/adding-a-feature/SKILL.md +9 -0
  147. package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +18 -6
  148. package/dist/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  149. package/dist/templates/default/react-router.config.ts +18 -0
  150. package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +18 -6
  151. package/dist/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  152. package/dist/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +9 -0
  153. package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +18 -6
  154. package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  155. package/dist/templates/workspace-core/.agents/skills/workspace-conventions/SKILL.md +7 -0
  156. package/package.json +3 -2
  157. package/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +9 -0
  158. package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +18 -6
  159. package/src/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  160. package/src/templates/default/.agents/skills/adding-a-feature/SKILL.md +9 -0
  161. package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +18 -6
  162. package/src/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  163. package/src/templates/default/react-router.config.ts +18 -0
  164. package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +18 -6
  165. package/src/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  166. package/src/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +9 -0
  167. package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +18 -6
  168. package/src/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +8 -1
  169. package/src/templates/workspace-core/.agents/skills/workspace-conventions/SKILL.md +7 -0
@@ -0,0 +1,816 @@
1
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore, } from "react";
2
+ export const CHAT_FIRST_MODE_STORAGE_KEY = "agent-native:chat-first-mode:v1";
3
+ export const CHAT_FIRST_APP_LAYOUT_STORAGE_KEY = "agent-native:chat-first-app-layout:v1";
4
+ export const CHAT_FIRST_SURFACE_TABS_STORAGE_KEY = "agent-native:chat-first-surface-tabs:v1";
5
+ export const CHAT_FIRST_SURFACE_WIDTH_STORAGE_KEY = "agent-native:chat-first-surface-width:v2";
6
+ export const CHAT_FIRST_SURFACE_PANEL_STORAGE_KEY = "agent-native:chat-first-surface-panel:v1";
7
+ export const CHAT_FIRST_MODE_CHANGED_EVENT = "agentNative:chatFirstModeChanged";
8
+ export const CHAT_FIRST_OPEN_APP_EVENT = "agentNative:openApp";
9
+ export const CHAT_FIRST_OPEN_BROWSER_EVENT = "agentNative:openBrowser";
10
+ export const CHAT_FIRST_WATCH_SESSION_EVENT = "agentNative:watchSession";
11
+ export const CHAT_FIRST_SURFACE_CATALOG = [
12
+ {
13
+ kind: "browser",
14
+ label: "Browser",
15
+ description: "Open a regular web page with address-bar controls.",
16
+ availability: "both",
17
+ },
18
+ {
19
+ kind: "terminal",
20
+ label: "Terminal",
21
+ description: "Run commands beside the conversation.",
22
+ availability: "deferred",
23
+ disabledReason: "Deferred in this release: the existing terminal is a standalone host surface; a shared PTY lifecycle is needed for an embedded pane.",
24
+ },
25
+ {
26
+ kind: "files",
27
+ label: "Files",
28
+ description: "Browse the active workspace tree.",
29
+ availability: "deferred",
30
+ disabledReason: "Deferred in this release: neither client exposes a bounded workspace tree to this shared pane contract yet.",
31
+ },
32
+ {
33
+ kind: "diff",
34
+ label: "Diff",
35
+ description: "Review the current changeset.",
36
+ availability: "deferred",
37
+ disabledReason: "Deferred in this release: diff blocks remain available in chat while a shared changed-file source is defined.",
38
+ },
39
+ {
40
+ kind: "side-chat",
41
+ label: "Side chat",
42
+ description: "Watch and message another session beside the main chat.",
43
+ availability: "both",
44
+ disabledReason: "Choose Watch and message from any chat row.",
45
+ },
46
+ {
47
+ kind: "agents",
48
+ label: "Agents",
49
+ description: "Review agent sessions and watch one beside the conversation.",
50
+ availability: "both",
51
+ disabledReason: "Review recent runs or watch one beside this conversation.",
52
+ },
53
+ ];
54
+ const CHAT_FIRST_SURFACE_KINDS = [
55
+ "app",
56
+ "browser",
57
+ "terminal",
58
+ "diff",
59
+ "files",
60
+ "side-chat",
61
+ "agents",
62
+ ];
63
+ export function chatFirstSurfaceTabId(kind, value) {
64
+ return `${kind}:${value}`;
65
+ }
66
+ export const CHAT_FIRST_SURFACE_WIDTH_DEFAULT = 380;
67
+ export const CHAT_FIRST_SURFACE_WIDTH_MIN = 320;
68
+ /**
69
+ * The first-run rail is intentionally opinionated. These are the five
70
+ * everyday workspace apps shown before the user expands the rest of the
71
+ * catalog; an explicit ordered layout still wins after the first change.
72
+ */
73
+ export const CHAT_FIRST_DEFAULT_APP_IDS = [
74
+ "content",
75
+ "design",
76
+ "mail",
77
+ "calendar",
78
+ "clips",
79
+ ];
80
+ export function clampChatFirstSurfaceWidth(value, viewportWidth = typeof window === "undefined"
81
+ ? Number.POSITIVE_INFINITY
82
+ : window.innerWidth) {
83
+ const max = Math.max(CHAT_FIRST_SURFACE_WIDTH_MIN, Math.floor(viewportWidth * 0.7));
84
+ return Math.min(max, Math.max(CHAT_FIRST_SURFACE_WIDTH_MIN, Math.round(value)));
85
+ }
86
+ function surfaceWidthStorageKey(scope) {
87
+ return `${CHAT_FIRST_SURFACE_WIDTH_STORAGE_KEY}:${normalizedSurfaceScope(scope)}`;
88
+ }
89
+ export function readChatFirstSurfaceWidth(scope = "default", storage = browserStorage()) {
90
+ if (!storage)
91
+ return CHAT_FIRST_SURFACE_WIDTH_DEFAULT;
92
+ try {
93
+ const raw = storage.getItem(surfaceWidthStorageKey(scope));
94
+ if (!raw)
95
+ return CHAT_FIRST_SURFACE_WIDTH_DEFAULT;
96
+ const value = Number(raw);
97
+ return Number.isFinite(value)
98
+ ? clampChatFirstSurfaceWidth(value)
99
+ : CHAT_FIRST_SURFACE_WIDTH_DEFAULT;
100
+ }
101
+ catch {
102
+ return CHAT_FIRST_SURFACE_WIDTH_DEFAULT;
103
+ }
104
+ }
105
+ export function writeChatFirstSurfaceWidth(width, scope = "default", storage = browserStorage()) {
106
+ if (!storage)
107
+ return { ok: false, reason: "unavailable" };
108
+ try {
109
+ storage.setItem(surfaceWidthStorageKey(scope), String(clampChatFirstSurfaceWidth(width)));
110
+ return { ok: true };
111
+ }
112
+ catch {
113
+ return { ok: false, reason: "write-failed" };
114
+ }
115
+ }
116
+ export function useChatFirstSurfaceResize(scope = "default") {
117
+ const [width, setWidth] = useState(() => readChatFirstSurfaceWidth(scope));
118
+ const [isResizing, setIsResizing] = useState(false);
119
+ const widthRef = useRef(width);
120
+ const cleanupRef = useRef(null);
121
+ widthRef.current = width;
122
+ useEffect(() => {
123
+ cleanupRef.current?.();
124
+ cleanupRef.current = null;
125
+ const next = readChatFirstSurfaceWidth(scope);
126
+ widthRef.current = next;
127
+ setWidth(next);
128
+ setIsResizing(false);
129
+ return () => cleanupRef.current?.();
130
+ }, [scope]);
131
+ const onPointerDown = useCallback((event) => {
132
+ if (typeof window === "undefined")
133
+ return;
134
+ event.preventDefault();
135
+ cleanupRef.current?.();
136
+ const startX = event.clientX;
137
+ const startWidth = widthRef.current;
138
+ setIsResizing(true);
139
+ const handleMove = (moveEvent) => {
140
+ const next = clampChatFirstSurfaceWidth(startWidth - (moveEvent.clientX - startX));
141
+ widthRef.current = next;
142
+ setWidth(next);
143
+ };
144
+ const handleUp = () => {
145
+ window.removeEventListener("pointermove", handleMove);
146
+ window.removeEventListener("pointerup", handleUp);
147
+ cleanupRef.current = null;
148
+ setIsResizing(false);
149
+ writeChatFirstSurfaceWidth(widthRef.current, scope);
150
+ };
151
+ cleanupRef.current = () => {
152
+ window.removeEventListener("pointermove", handleMove);
153
+ window.removeEventListener("pointerup", handleUp);
154
+ };
155
+ window.addEventListener("pointermove", handleMove);
156
+ window.addEventListener("pointerup", handleUp, { once: true });
157
+ }, [scope]);
158
+ return { width, isResizing, onPointerDown };
159
+ }
160
+ function surfacePanelStorageKey(scope) {
161
+ return `${CHAT_FIRST_SURFACE_PANEL_STORAGE_KEY}:${normalizedSurfaceScope(scope)}`;
162
+ }
163
+ function createChatFirstSurfacePanelStore(scope) {
164
+ let state = { open: false };
165
+ const listeners = new Set();
166
+ try {
167
+ state = {
168
+ open: browserStorage()?.getItem(surfacePanelStorageKey(scope)) === "true",
169
+ };
170
+ // coercion-ok: device-local panel preference failure falls back to the closed in-memory state.
171
+ }
172
+ catch {
173
+ // The panel is an optional device preference; the live default is closed.
174
+ }
175
+ const publish = (open) => {
176
+ if (state.open === open)
177
+ return;
178
+ state = { open };
179
+ try {
180
+ browserStorage()?.setItem(surfacePanelStorageKey(scope), String(open));
181
+ // coercion-ok: device-local persistence failure leaves the in-memory toggle usable.
182
+ }
183
+ catch {
184
+ // Keep the in-memory toggle usable when device storage is unavailable.
185
+ }
186
+ listeners.forEach((listener) => listener());
187
+ };
188
+ return {
189
+ getSnapshot: () => state,
190
+ subscribe: (listener) => {
191
+ listeners.add(listener);
192
+ return () => listeners.delete(listener);
193
+ },
194
+ setOpen: publish,
195
+ toggle: () => publish(!state.open),
196
+ };
197
+ }
198
+ const chatFirstSurfacePanelStores = new Map();
199
+ export function getChatFirstSurfacePanelStore(scope = "default") {
200
+ const key = normalizedSurfaceScope(scope);
201
+ const existing = chatFirstSurfacePanelStores.get(key);
202
+ if (existing)
203
+ return existing;
204
+ const store = createChatFirstSurfacePanelStore(key);
205
+ chatFirstSurfacePanelStores.set(key, store);
206
+ return store;
207
+ }
208
+ export function useChatFirstSurfacePanel(scope = "default") {
209
+ const store = getChatFirstSurfacePanelStore(scope);
210
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
211
+ useEffect(() => {
212
+ const handleKeyDown = (event) => {
213
+ if ((event.metaKey || event.ctrlKey) &&
214
+ event.altKey &&
215
+ event.key.toLowerCase() === "b") {
216
+ event.preventDefault();
217
+ store.toggle();
218
+ }
219
+ };
220
+ window.addEventListener("keydown", handleKeyDown);
221
+ return () => window.removeEventListener("keydown", handleKeyDown);
222
+ }, [store]);
223
+ return { ...state, setOpen: store.setOpen, toggle: store.toggle };
224
+ }
225
+ function browserStorage() {
226
+ if (typeof window === "undefined")
227
+ return null;
228
+ try {
229
+ return window.localStorage;
230
+ // coercion-ok: callers receive null and expose unavailable device storage explicitly.
231
+ }
232
+ catch {
233
+ return null;
234
+ }
235
+ }
236
+ export function readChatFirstModeState(storage = browserStorage()) {
237
+ if (!storage) {
238
+ return { enabled: false, availability: "unavailable" };
239
+ }
240
+ try {
241
+ return {
242
+ enabled: storage.getItem(CHAT_FIRST_MODE_STORAGE_KEY) === "true",
243
+ availability: "available",
244
+ };
245
+ }
246
+ catch {
247
+ return { enabled: false, availability: "unavailable" };
248
+ }
249
+ }
250
+ export function readChatFirstMode(storage = browserStorage()) {
251
+ return readChatFirstModeState(storage).enabled;
252
+ }
253
+ export function writeChatFirstMode(enabled, storage = browserStorage()) {
254
+ if (!storage)
255
+ return { ok: false, reason: "unavailable" };
256
+ try {
257
+ storage.setItem(CHAT_FIRST_MODE_STORAGE_KEY, String(enabled));
258
+ return { ok: true };
259
+ }
260
+ catch {
261
+ return { ok: false, reason: "write-failed" };
262
+ }
263
+ }
264
+ const EMPTY_CHAT_FIRST_APP_LAYOUT = {
265
+ pinnedIds: [],
266
+ orderedIds: [],
267
+ };
268
+ export function readChatFirstAppLayout(storage = browserStorage()) {
269
+ if (!storage)
270
+ return { ...EMPTY_CHAT_FIRST_APP_LAYOUT };
271
+ try {
272
+ const raw = storage.getItem(CHAT_FIRST_APP_LAYOUT_STORAGE_KEY);
273
+ if (!raw)
274
+ return { ...EMPTY_CHAT_FIRST_APP_LAYOUT };
275
+ const parsed = JSON.parse(raw);
276
+ if (!parsed || typeof parsed !== "object") {
277
+ return { ...EMPTY_CHAT_FIRST_APP_LAYOUT };
278
+ }
279
+ const value = parsed;
280
+ const ids = (candidate) => Array.isArray(candidate)
281
+ ? candidate.filter((item) => typeof item === "string" && item.trim().length > 0)
282
+ : [];
283
+ return {
284
+ pinnedIds: [...new Set(ids(value.pinnedIds))],
285
+ orderedIds: [...new Set(ids(value.orderedIds))],
286
+ };
287
+ }
288
+ catch {
289
+ return { ...EMPTY_CHAT_FIRST_APP_LAYOUT };
290
+ }
291
+ }
292
+ export function writeChatFirstAppLayout(layout, storage = browserStorage()) {
293
+ if (!storage)
294
+ return { ok: false, reason: "unavailable" };
295
+ try {
296
+ storage.setItem(CHAT_FIRST_APP_LAYOUT_STORAGE_KEY, JSON.stringify({
297
+ pinnedIds: [...new Set(layout.pinnedIds.filter(Boolean))],
298
+ orderedIds: [...new Set(layout.orderedIds.filter(Boolean))],
299
+ }));
300
+ return { ok: true };
301
+ }
302
+ catch {
303
+ return { ok: false, reason: "write-failed" };
304
+ }
305
+ }
306
+ export function orderChatFirstAppIds(appIds, layout) {
307
+ const available = new Set(appIds);
308
+ const preferredDefaults = CHAT_FIRST_DEFAULT_APP_IDS.filter((id) => available.has(id));
309
+ const preferredDefaultSet = new Set(preferredDefaults);
310
+ const fallbackOrder = [
311
+ ...preferredDefaults,
312
+ ...appIds.filter((id) => !preferredDefaultSet.has(id)),
313
+ ];
314
+ const ordered = [
315
+ ...layout.orderedIds.filter((id) => available.has(id)),
316
+ ...fallbackOrder.filter((id) => !layout.orderedIds.includes(id)),
317
+ ];
318
+ const pinned = new Set(layout.pinnedIds.filter((id) => available.has(id)));
319
+ // `orderedIds` is the single source of positional truth. Pinning changes
320
+ // presentation only; it must not rewrite the user's drag order or make an
321
+ // unpinned app jump to the fallback order.
322
+ return [
323
+ ...ordered.filter((id) => pinned.has(id)),
324
+ ...ordered.filter((id) => !pinned.has(id)),
325
+ ];
326
+ }
327
+ export function resolveChatFirstBrowserTarget(detail) {
328
+ const rawUrl = detail.url?.trim();
329
+ if (!rawUrl)
330
+ return { status: "unresolved", reason: "empty-detail" };
331
+ const url = parseAbsoluteHttpUrl(rawUrl);
332
+ if (!url || url.username || url.password) {
333
+ return { status: "unresolved", reason: "invalid-url" };
334
+ }
335
+ return {
336
+ status: "ready",
337
+ target: {
338
+ url: url.href,
339
+ ...(detail.title?.trim() ? { title: detail.title.trim() } : {}),
340
+ },
341
+ };
342
+ }
343
+ const chatFirstOpenAppListeners = new Set();
344
+ const chatFirstOpenBrowserListeners = new Set();
345
+ function normalizeSessionId(value) {
346
+ if (typeof value !== "string")
347
+ return null;
348
+ const id = value.trim();
349
+ return id || null;
350
+ }
351
+ /** Resolve ids from run/thread payloads without treating an absent id as a session. */
352
+ export function resolveChatFirstSessionId(value) {
353
+ if (typeof value === "string")
354
+ return normalizeSessionId(value);
355
+ if (!value || typeof value !== "object")
356
+ return null;
357
+ const record = value;
358
+ for (const key of ["sessionId", "runId", "threadId", "id"]) {
359
+ const id = normalizeSessionId(record[key]);
360
+ if (id)
361
+ return id;
362
+ }
363
+ return null;
364
+ }
365
+ export function normalizeChatFirstSessionReference(value) {
366
+ const sessionId = resolveChatFirstSessionId(value);
367
+ if (!sessionId)
368
+ return null;
369
+ if (!value || typeof value !== "object")
370
+ return { sessionId };
371
+ const record = value;
372
+ const title = normalizeSessionId(record.title);
373
+ const goalId = normalizeSessionId(record.goalId);
374
+ const sourceSessionId = normalizeSessionId(record.sourceSessionId);
375
+ const kind = record.kind === "code-agent" ||
376
+ record.kind === "agent-chat" ||
377
+ record.kind === "external"
378
+ ? record.kind
379
+ : undefined;
380
+ return {
381
+ sessionId,
382
+ ...(title ? { title } : {}),
383
+ ...(kind ? { kind } : {}),
384
+ ...(goalId ? { goalId } : {}),
385
+ ...(sourceSessionId ? { sourceSessionId } : {}),
386
+ };
387
+ }
388
+ function createChatFirstSessionWatchStore() {
389
+ let state = { target: null };
390
+ const listeners = new Set();
391
+ return {
392
+ getSnapshot: () => state,
393
+ subscribe: (listener) => {
394
+ listeners.add(listener);
395
+ return () => listeners.delete(listener);
396
+ },
397
+ hasSubscribers: () => listeners.size > 0,
398
+ open: (target) => {
399
+ state = { target };
400
+ listeners.forEach((listener) => listener());
401
+ },
402
+ close: () => {
403
+ if (!state.target)
404
+ return;
405
+ state = { target: null };
406
+ listeners.forEach((listener) => listener());
407
+ },
408
+ };
409
+ }
410
+ const chatFirstSessionWatchStore = createChatFirstSessionWatchStore();
411
+ export function getChatFirstSessionWatchStore() {
412
+ return chatFirstSessionWatchStore;
413
+ }
414
+ export function useChatFirstSessionWatch() {
415
+ return useSyncExternalStore(chatFirstSessionWatchStore.subscribe, chatFirstSessionWatchStore.getSnapshot, chatFirstSessionWatchStore.getSnapshot);
416
+ }
417
+ export function subscribeChatFirstSessionWatch(listener) {
418
+ if (typeof window === "undefined")
419
+ return () => undefined;
420
+ listenersForWatch += 1;
421
+ const handleEvent = (event) => {
422
+ const target = normalizeChatFirstSessionReference(event.detail);
423
+ if (target)
424
+ listener(target);
425
+ };
426
+ window.addEventListener(CHAT_FIRST_WATCH_SESSION_EVENT, handleEvent);
427
+ return () => {
428
+ listenersForWatch = Math.max(0, listenersForWatch - 1);
429
+ window.removeEventListener(CHAT_FIRST_WATCH_SESSION_EVENT, handleEvent);
430
+ };
431
+ }
432
+ export function emitChatFirstSessionWatch(detail) {
433
+ const target = normalizeChatFirstSessionReference(detail);
434
+ if (!target)
435
+ return { delivered: false, reason: "empty-detail" };
436
+ const delivered = chatFirstSessionWatchStore.hasSubscribers() || listenersForWatch > 0;
437
+ chatFirstSessionWatchStore.open(target);
438
+ if (typeof window !== "undefined") {
439
+ window.dispatchEvent(new CustomEvent(CHAT_FIRST_WATCH_SESSION_EVENT, { detail: target }));
440
+ }
441
+ return delivered
442
+ ? { delivered: true }
443
+ : { delivered: false, reason: "no-listener" };
444
+ }
445
+ let listenersForWatch = 0;
446
+ export function closeChatFirstSessionWatch() {
447
+ chatFirstSessionWatchStore.close();
448
+ }
449
+ function readChatFirstSurfaceTabsState(scope) {
450
+ const storage = browserStorage();
451
+ if (!storage)
452
+ return { tabs: [], activeTabId: null };
453
+ try {
454
+ const raw = storage.getItem(CHAT_FIRST_SURFACE_TABS_STORAGE_KEY);
455
+ if (!raw)
456
+ return { tabs: [], activeTabId: null };
457
+ const parsed = JSON.parse(raw);
458
+ if (!parsed || typeof parsed !== "object") {
459
+ return { tabs: [], activeTabId: null };
460
+ }
461
+ const scoped = parsed[scope];
462
+ if (!scoped || typeof scoped !== "object") {
463
+ return { tabs: [], activeTabId: null };
464
+ }
465
+ const value = scoped;
466
+ const tabs = Array.isArray(value.tabs)
467
+ ? value.tabs.flatMap((candidate) => {
468
+ const tab = normalizePersistedChatFirstSurfaceTab(candidate);
469
+ return tab ? [tab] : [];
470
+ })
471
+ : [];
472
+ const activeTabId = typeof value.activeTabId === "string" &&
473
+ tabs.some((tab) => tab.id === value.activeTabId)
474
+ ? value.activeTabId
475
+ : (tabs[0]?.id ?? null);
476
+ return { tabs, activeTabId };
477
+ }
478
+ catch {
479
+ return { tabs: [], activeTabId: null };
480
+ }
481
+ }
482
+ function normalizePersistedChatFirstSurfaceTab(candidate) {
483
+ if (!candidate || typeof candidate !== "object")
484
+ return null;
485
+ const value = candidate;
486
+ const id = typeof value.id === "string" ? value.id.trim() : "";
487
+ const title = typeof value.title === "string" ? value.title.trim() : "";
488
+ const kind = value.kind;
489
+ if (!id ||
490
+ !title ||
491
+ typeof kind !== "string" ||
492
+ !CHAT_FIRST_SURFACE_KINDS.includes(kind)) {
493
+ return null;
494
+ }
495
+ const tab = value;
496
+ if (kind === "app") {
497
+ const appId = typeof value.appId === "string" ? value.appId.trim() : "";
498
+ if (!appId)
499
+ return null;
500
+ if (value.path !== undefined &&
501
+ (typeof value.path !== "string" || normalizedPath(value.path) === null)) {
502
+ return null;
503
+ }
504
+ return { ...tab, id, title, appId };
505
+ }
506
+ if (kind === "browser") {
507
+ const url = typeof value.url === "string" ? value.url.trim() : "";
508
+ const parsed = parseAbsoluteHttpUrl(url);
509
+ if (!parsed || parsed.username || parsed.password)
510
+ return null;
511
+ return { ...tab, id, title, url: parsed.href };
512
+ }
513
+ if (kind === "side-chat") {
514
+ const session = normalizeChatFirstSessionReference(value.session);
515
+ return session ? { ...tab, id, title, session } : null;
516
+ }
517
+ return { ...tab, id, title, kind: kind };
518
+ }
519
+ function writeChatFirstSurfaceTabsState(scope, state) {
520
+ const storage = browserStorage();
521
+ if (!storage)
522
+ return;
523
+ try {
524
+ const raw = storage.getItem(CHAT_FIRST_SURFACE_TABS_STORAGE_KEY);
525
+ const parsed = raw ? JSON.parse(raw) : {};
526
+ const all = parsed && typeof parsed === "object"
527
+ ? parsed
528
+ : {};
529
+ storage.setItem(CHAT_FIRST_SURFACE_TABS_STORAGE_KEY, JSON.stringify({ ...all, [scope]: state }));
530
+ // coercion-ok: device-local tab restoration failure leaves the live tab store usable.
531
+ }
532
+ catch {
533
+ // Device-local tab restoration is optional; the live store remains usable.
534
+ }
535
+ }
536
+ function createChatFirstSurfaceTabsStore(scope) {
537
+ let state = readChatFirstSurfaceTabsState(scope);
538
+ const listeners = new Set();
539
+ const publish = (next) => {
540
+ state = next;
541
+ writeChatFirstSurfaceTabsState(scope, state);
542
+ listeners.forEach((listener) => listener());
543
+ };
544
+ const activeIndex = (tabId) => state.tabs.findIndex((tab) => tab.id === tabId);
545
+ return {
546
+ getSnapshot: () => state,
547
+ subscribe: (listener) => {
548
+ listeners.add(listener);
549
+ return () => listeners.delete(listener);
550
+ },
551
+ open: (tab) => {
552
+ const existingIndex = activeIndex(tab.id);
553
+ if (existingIndex >= 0) {
554
+ const tabs = state.tabs.map((current, index) => index === existingIndex ? tab : current);
555
+ publish({ tabs, activeTabId: tab.id });
556
+ return;
557
+ }
558
+ publish({
559
+ tabs: [...state.tabs, tab],
560
+ activeTabId: tab.id,
561
+ });
562
+ },
563
+ activate: (tabId) => {
564
+ if (activeIndex(tabId) < 0 || state.activeTabId === tabId)
565
+ return;
566
+ publish({ ...state, activeTabId: tabId });
567
+ },
568
+ close: (tabId) => {
569
+ const index = activeIndex(tabId);
570
+ if (index < 0)
571
+ return;
572
+ const tabs = state.tabs.filter((tab) => tab.id !== tabId);
573
+ const activeTabId = state.activeTabId !== tabId
574
+ ? state.activeTabId
575
+ : (tabs[index - 1]?.id ?? tabs[index]?.id ?? null);
576
+ publish({ tabs, activeTabId });
577
+ },
578
+ closeOthers: (tabId) => {
579
+ if (activeIndex(tabId) < 0)
580
+ return;
581
+ const tab = state.tabs.find((candidate) => candidate.id === tabId);
582
+ if (!tab)
583
+ return;
584
+ publish({ tabs: [tab], activeTabId: tab.id });
585
+ },
586
+ closeToRight: (tabId) => {
587
+ const index = activeIndex(tabId);
588
+ if (index < 0)
589
+ return;
590
+ const tabs = state.tabs.slice(0, index + 1);
591
+ const activeTabId = tabs.some((tab) => tab.id === state.activeTabId)
592
+ ? state.activeTabId
593
+ : (tabs[index]?.id ?? null);
594
+ publish({ tabs, activeTabId });
595
+ },
596
+ closeAll: () => publish({ tabs: [], activeTabId: null }),
597
+ };
598
+ }
599
+ const chatFirstSurfaceTabsStores = new Map();
600
+ function normalizedSurfaceScope(scope) {
601
+ const value = scope.trim();
602
+ return value || "default";
603
+ }
604
+ export function getChatFirstSurfaceTabsStore(scope = "default") {
605
+ const key = normalizedSurfaceScope(scope);
606
+ const existing = chatFirstSurfaceTabsStores.get(key);
607
+ if (existing)
608
+ return existing;
609
+ const store = createChatFirstSurfaceTabsStore(key);
610
+ chatFirstSurfaceTabsStores.set(key, store);
611
+ return store;
612
+ }
613
+ export function useChatFirstSurfaceTabs(scope = "default") {
614
+ const store = getChatFirstSurfaceTabsStore(scope);
615
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
616
+ }
617
+ export function subscribeChatFirstOpenApp(listener) {
618
+ if (typeof window === "undefined")
619
+ return () => undefined;
620
+ const subscription = Symbol("chat-first-open-app");
621
+ const handleEvent = (event) => {
622
+ const detail = event.detail;
623
+ if (detail && typeof detail === "object")
624
+ listener(detail);
625
+ };
626
+ chatFirstOpenAppListeners.add(subscription);
627
+ window.addEventListener(CHAT_FIRST_OPEN_APP_EVENT, handleEvent);
628
+ return () => {
629
+ chatFirstOpenAppListeners.delete(subscription);
630
+ window.removeEventListener(CHAT_FIRST_OPEN_APP_EVENT, handleEvent);
631
+ };
632
+ }
633
+ export function emitChatFirstOpenApp(detail) {
634
+ if (!detail.app && !detail.url && !detail.path && !detail.view) {
635
+ return { delivered: false, reason: "empty-detail" };
636
+ }
637
+ if (typeof window === "undefined") {
638
+ return { delivered: false, reason: "no-listener" };
639
+ }
640
+ const delivered = chatFirstOpenAppListeners.size > 0;
641
+ window.dispatchEvent(new CustomEvent(CHAT_FIRST_OPEN_APP_EVENT, {
642
+ detail,
643
+ }));
644
+ return delivered
645
+ ? { delivered: true }
646
+ : { delivered: false, reason: "no-listener" };
647
+ }
648
+ export function subscribeChatFirstOpenBrowser(listener) {
649
+ if (typeof window === "undefined")
650
+ return () => undefined;
651
+ const subscription = Symbol("chat-first-open-browser");
652
+ const handleEvent = (event) => {
653
+ const detail = event.detail;
654
+ if (detail && typeof detail === "object")
655
+ listener(detail);
656
+ };
657
+ chatFirstOpenBrowserListeners.add(subscription);
658
+ window.addEventListener(CHAT_FIRST_OPEN_BROWSER_EVENT, handleEvent);
659
+ return () => {
660
+ chatFirstOpenBrowserListeners.delete(subscription);
661
+ window.removeEventListener(CHAT_FIRST_OPEN_BROWSER_EVENT, handleEvent);
662
+ };
663
+ }
664
+ export function emitChatFirstOpenBrowser(detail) {
665
+ if (!detail.url?.trim()) {
666
+ return { delivered: false, reason: "empty-detail" };
667
+ }
668
+ if (typeof window === "undefined") {
669
+ return { delivered: false, reason: "no-listener" };
670
+ }
671
+ const delivered = chatFirstOpenBrowserListeners.size > 0;
672
+ window.dispatchEvent(new CustomEvent(CHAT_FIRST_OPEN_BROWSER_EVENT, {
673
+ detail,
674
+ }));
675
+ return delivered
676
+ ? { delivered: true }
677
+ : { delivered: false, reason: "no-listener" };
678
+ }
679
+ function normalizedPath(value) {
680
+ const path = value?.trim();
681
+ if (!path || !path.startsWith("/") || path.startsWith("//"))
682
+ return null;
683
+ if (path.startsWith("/\\") || /[\u0000-\u001f\u007f]/.test(path)) {
684
+ return null;
685
+ }
686
+ if (/^\/[a-z][a-z0-9+.-]*:/i.test(path))
687
+ return null;
688
+ if (path.split(/[?#]/, 1)[0]?.split("/").includes(".."))
689
+ return null;
690
+ return path;
691
+ }
692
+ function parseAbsoluteHttpUrl(value) {
693
+ try {
694
+ const url = new URL(value);
695
+ return url.protocol === "http:" || url.protocol === "https:" ? url : null;
696
+ // coercion-ok: invalid URL input is returned as null and rejected by the resolver.
697
+ }
698
+ catch {
699
+ return null;
700
+ }
701
+ }
702
+ function registrationUrls(app, currentOrigin) {
703
+ const values = [app.url, app.devUrl];
704
+ if (app.path && currentOrigin)
705
+ values.push(`${currentOrigin}${app.path}`);
706
+ return values.flatMap((value) => {
707
+ if (!value)
708
+ return [];
709
+ const absolute = parseAbsoluteHttpUrl(value);
710
+ if (absolute)
711
+ return [absolute];
712
+ if (value.startsWith("/") && currentOrigin) {
713
+ const relative = parseAbsoluteHttpUrl(`${currentOrigin}${value}`);
714
+ return relative ? [relative] : [];
715
+ }
716
+ return [];
717
+ });
718
+ }
719
+ function appMatchesUrl(app, targetUrl, currentOrigin) {
720
+ for (const registeredUrl of registrationUrls(app, currentOrigin)) {
721
+ if (registeredUrl.origin !== targetUrl.origin)
722
+ continue;
723
+ const basePath = registeredUrl.pathname.replace(/\/+$/, "") || "/";
724
+ if (basePath !== "/" &&
725
+ targetUrl.pathname !== basePath &&
726
+ !targetUrl.pathname.startsWith(`${basePath}/`)) {
727
+ continue;
728
+ }
729
+ return registeredUrl;
730
+ }
731
+ return null;
732
+ }
733
+ function appRelativeUrlPath(targetUrl, registeredUrl) {
734
+ const basePath = registeredUrl.pathname.replace(/\/+$/, "") || "/";
735
+ const pathname = basePath === "/"
736
+ ? targetUrl.pathname
737
+ : targetUrl.pathname === basePath
738
+ ? "/"
739
+ : targetUrl.pathname.slice(basePath.length) || "/";
740
+ return `${pathname}${targetUrl.search}${targetUrl.hash}`;
741
+ }
742
+ function viewPath(appId, view) {
743
+ const params = new URLSearchParams({ app: appId, view });
744
+ return `/_agent-native/open?${params.toString()}`;
745
+ }
746
+ export function resolveChatFirstAppTarget(detail, apps, options = {}) {
747
+ if (!detail.app && !detail.url && !detail.path && !detail.view) {
748
+ return { status: "unresolved", reason: "empty-detail" };
749
+ }
750
+ const availableApps = apps.filter((app) => app.enabled !== false);
751
+ const requestedId = detail.app?.trim().toLowerCase();
752
+ let app = requestedId
753
+ ? availableApps.find((candidate) => candidate.id.toLowerCase() === requestedId ||
754
+ candidate.name?.trim().toLowerCase() === requestedId)
755
+ : undefined;
756
+ const rawUrl = detail.url?.trim();
757
+ let targetUrl = null;
758
+ if (rawUrl) {
759
+ targetUrl = parseAbsoluteHttpUrl(rawUrl);
760
+ if (!targetUrl) {
761
+ const relativePath = normalizedPath(rawUrl);
762
+ if (!app || !relativePath) {
763
+ return { status: "unresolved", reason: "invalid-url" };
764
+ }
765
+ }
766
+ }
767
+ if (targetUrl) {
768
+ if (app) {
769
+ const registeredUrl = appMatchesUrl(app, targetUrl, options.currentOrigin);
770
+ if (!registeredUrl) {
771
+ return { status: "unresolved", reason: "invalid-url" };
772
+ }
773
+ }
774
+ else {
775
+ const matchingApp = availableApps
776
+ .map((candidate) => ({
777
+ app: candidate,
778
+ registeredUrl: appMatchesUrl(candidate, targetUrl, options.currentOrigin),
779
+ }))
780
+ .find((candidate) => candidate.registeredUrl);
781
+ if (!matchingApp) {
782
+ return { status: "unresolved", reason: "unknown-app" };
783
+ }
784
+ app = matchingApp.app;
785
+ }
786
+ }
787
+ if (!app)
788
+ return { status: "unresolved", reason: "unknown-app" };
789
+ let path;
790
+ if (targetUrl) {
791
+ const registeredUrl = appMatchesUrl(app, targetUrl, options.currentOrigin);
792
+ if (!registeredUrl) {
793
+ return { status: "unresolved", reason: "invalid-url" };
794
+ }
795
+ path = appRelativeUrlPath(targetUrl, registeredUrl);
796
+ }
797
+ else if (rawUrl) {
798
+ path = normalizedPath(rawUrl) ?? undefined;
799
+ }
800
+ else if (detail.path) {
801
+ path = normalizedPath(detail.path) ?? undefined;
802
+ if (!path)
803
+ return { status: "unresolved", reason: "invalid-url" };
804
+ }
805
+ else if (detail.view?.trim()) {
806
+ path = viewPath(app.id, detail.view.trim());
807
+ }
808
+ return {
809
+ status: "ready",
810
+ target: {
811
+ appId: app.id,
812
+ ...(path ? { path } : {}),
813
+ ...(detail.view?.trim() ? { view: detail.view.trim() } : {}),
814
+ },
815
+ };
816
+ }