@adminide-stack/yantra-mobile 12.0.64-alpha.1 → 12.0.64-alpha.2

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.
@@ -7,7 +7,10 @@ import {useSyncExternalStore,useMemo,useEffect}from'react';import {useChatStream
7
7
  isStreaming: false,
8
8
  messagesLoading: false,
9
9
  hasMessages: false,
10
- sendMessage: async () => void 0,
10
+ queuedMessages: [],
11
+ flushQueuedMessages: async () => void 0,
12
+ discardQueuedMessages: () => void 0,
13
+ sendMessage: async () => "failed",
11
14
  cancel: () => void 0,
12
15
  clearMessages: () => void 0
13
16
  };
@@ -15,7 +18,7 @@ let snapshot = EMPTY;
15
18
  const listeners = /* @__PURE__ */ new Set();
16
19
  function publish(next) {
17
20
  const prev = snapshot;
18
- if (prev.messages === next.messages && prev.response === next.response && prev.quickReply === next.quickReply && prev.error === next.error && prev.isLoading === next.isLoading && prev.isStreaming === next.isStreaming && prev.messagesLoading === next.messagesLoading && prev.sendMessage === next.sendMessage) {
21
+ if (prev.messages === next.messages && prev.response === next.response && prev.quickReply === next.quickReply && prev.error === next.error && prev.isLoading === next.isLoading && prev.isStreaming === next.isStreaming && prev.messagesLoading === next.messagesLoading && prev.queuedMessages === next.queuedMessages && prev.sendMessage === next.sendMessage) {
19
22
  snapshot = next;
20
23
  return;
21
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ChatLiveStreamContext.js","sources":["../../src/contexts/ChatLiveStreamContext.tsx"],"sourcesContent":["/**\n * App-wide owner of the mobile chat stream.\n *\n * `useChatStream` used to live on ChatScreen. Opening history unmounted that\n * screen, which tore down GraphQL subscriptions — the brain's provisional\n * reply vanished and the published agent stream never reattached. Web keeps\n * ChatLayout mounted; this engine does the same: it stays under\n * `CdecliConnectionProvider` and Chat only *reads* the live snapshot.\n */\nimport React, { useEffect, useMemo } from 'react';\nimport { useSyncExternalStore } from 'react';\nimport { useChatStream, type UseChatStreamRouting } from '../hooks/useChatStream';\nimport { useCdecliConnection } from './CdecliConnectionContext';\n\nexport type ChatLiveStreamSnapshot = ReturnType<typeof useChatStream>;\n\nconst EMPTY: ChatLiveStreamSnapshot = {\n messages: [],\n response: '',\n quickReply: '',\n error: null,\n isLoading: false,\n isStreaming: false,\n messagesLoading: false,\n hasMessages: false,\n sendMessage: async () => undefined,\n cancel: () => undefined,\n clearMessages: () => undefined,\n};\n\nlet snapshot: ChatLiveStreamSnapshot = EMPTY;\nconst listeners = new Set<() => void>();\n\nfunction publish(next: ChatLiveStreamSnapshot) {\n const prev = snapshot;\n if (\n prev.messages === next.messages &&\n prev.response === next.response &&\n prev.quickReply === next.quickReply &&\n prev.error === next.error &&\n prev.isLoading === next.isLoading &&\n prev.isStreaming === next.isStreaming &&\n prev.messagesLoading === next.messagesLoading &&\n prev.sendMessage === next.sendMessage\n ) {\n snapshot = next;\n return;\n }\n snapshot = next;\n listeners.forEach((fn) => fn());\n}\n\nfunction subscribe(listener: () => void) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nfunction getSnapshot() {\n return snapshot;\n}\n\n/** Keeps messenger subscriptions alive while the user is on history / home. */\nexport function ChatLiveStreamEngine() {\n const cdecli = useCdecliConnection();\n const routing: UseChatStreamRouting = useMemo(\n () => ({\n channelConnected: cdecli.channelConnected,\n accountId: cdecli.accountId,\n persistenceMode: cdecli.persistenceMode ?? 'backend',\n recoverAgentAuth: cdecli.recoverAgentAuth,\n }),\n [cdecli.channelConnected, cdecli.accountId, cdecli.persistenceMode, cdecli.recoverAgentAuth],\n );\n const stream = useChatStream(cdecli.activeChannelId ?? null, routing);\n useEffect(() => {\n publish(stream);\n }, [stream]);\n return null;\n}\n\nexport function useChatLiveStream(): ChatLiveStreamSnapshot {\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n"],"names":["_a"],"mappings":"gLAcA,MAAM,KAAgC,GAAA;AAAA,EACpC,UAAU,EAAC;AAAA,EACX,QAAU,EAAA,EAAA;AAAA,EACV,UAAY,EAAA,EAAA;AAAA,EACZ,KAAO,EAAA,IAAA;AAAA,EACP,SAAW,EAAA,KAAA;AAAA,EACX,WAAa,EAAA,KAAA;AAAA,EACb,eAAiB,EAAA,KAAA;AAAA,EACjB,WAAa,EAAA,KAAA;AAAA,EACb,aAAa,YAAY,MAAA;AAAA,EACzB,QAAQ,MAAM,MAAA;AAAA,EACd,eAAe,MAAM;AACvB,CAAA;AACA,IAAI,QAAmC,GAAA,KAAA;AACvC,MAAM,SAAA,uBAAgB,GAAgB,EAAA;AACtC,SAAS,QAAQ,IAA8B,EAAA;AAC7C,EAAA,MAAM,IAAO,GAAA,QAAA;AACb,EAAA,IAAI,IAAK,CAAA,QAAA,KAAa,IAAK,CAAA,QAAA,IAAY,IAAK,CAAA,QAAA,KAAa,IAAK,CAAA,QAAA,IAAY,IAAK,CAAA,UAAA,KAAe,IAAK,CAAA,UAAA,IAAc,KAAK,KAAU,KAAA,IAAA,CAAK,KAAS,IAAA,IAAA,CAAK,SAAc,KAAA,IAAA,CAAK,SAAa,IAAA,IAAA,CAAK,gBAAgB,IAAK,CAAA,WAAA,IAAe,IAAK,CAAA,eAAA,KAAoB,IAAK,CAAA,eAAA,IAAmB,IAAK,CAAA,WAAA,KAAgB,KAAK,WAAa,EAAA;AAClT,IAAW,QAAA,GAAA,IAAA;AACX,IAAA;AAAA;AAEF,EAAW,QAAA,GAAA,IAAA;AACX,EAAU,SAAA,CAAA,OAAA,CAAQ,CAAM,EAAA,KAAA,EAAA,EAAI,CAAA;AAC9B;AACA,SAAS,UAAU,QAAsB,EAAA;AACvC,EAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,EAAA,OAAO,MAAM;AACX,IAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,GAC3B;AACF;AACA,SAAS,WAAc,GAAA;AACrB,EAAO,OAAA,QAAA;AACT;AAGO,SAAS,oBAAuB,GAAA;AAjDvC,EAAA,IAAA,EAAA;AAkDE,EAAA,MAAM,SAAS,mBAAoB,EAAA;AACnC,EAAM,MAAA,OAAA,GAAgC,QAAQ,MAAG;AAnDnD,IAAAA,IAAAA,GAAAA;AAmDuD,IAAA,OAAA;AAAA,MACnD,kBAAkB,MAAO,CAAA,gBAAA;AAAA,MACzB,WAAW,MAAO,CAAA,SAAA;AAAA,MAClB,eAAiBA,EAAAA,CAAAA,GAAAA,GAAA,MAAO,CAAA,eAAA,KAAP,OAAAA,GAA0B,GAAA,SAAA;AAAA,MAC3C,kBAAkB,MAAO,CAAA;AAAA,KAC3B;AAAA,GAAI,EAAA,CAAC,OAAO,gBAAkB,EAAA,MAAA,CAAO,WAAW,MAAO,CAAA,eAAA,EAAiB,MAAO,CAAA,gBAAgB,CAAC,CAAA;AAChG,EAAA,MAAM,SAAS,aAAc,CAAA,CAAA,EAAA,GAAA,MAAA,CAAO,eAAP,KAAA,IAAA,GAAA,EAAA,GAA0B,MAAM,OAAO,CAAA;AACpE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,OAAA,CAAQ,MAAM,CAAA;AAAA,GAChB,EAAG,CAAC,MAAM,CAAC,CAAA;AACX,EAAO,OAAA,IAAA;AACT;AACO,SAAS,iBAA4C,GAAA;AAC1D,EAAO,OAAA,oBAAA,CAAqB,SAAW,EAAA,WAAA,EAAa,WAAW,CAAA;AACjE"}
1
+ {"version":3,"file":"ChatLiveStreamContext.js","sources":["../../src/contexts/ChatLiveStreamContext.tsx"],"sourcesContent":["/**\n * App-wide owner of the mobile chat stream.\n *\n * `useChatStream` used to live on ChatScreen. Opening history unmounted that\n * screen, which tore down GraphQL subscriptions — the brain's provisional\n * reply vanished and the published agent stream never reattached. Web keeps\n * ChatLayout mounted; this engine does the same: it stays under\n * `CdecliConnectionProvider` and Chat only *reads* the live snapshot.\n */\nimport React, { useEffect, useMemo } from 'react';\nimport { useSyncExternalStore } from 'react';\nimport { useChatStream, type UseChatStreamRouting } from '../hooks/useChatStream';\nimport { useCdecliConnection } from './CdecliConnectionContext';\n\nexport type ChatLiveStreamSnapshot = ReturnType<typeof useChatStream>;\n\nconst EMPTY: ChatLiveStreamSnapshot = {\n messages: [],\n response: '',\n quickReply: '',\n error: null,\n isLoading: false,\n isStreaming: false,\n messagesLoading: false,\n hasMessages: false,\n queuedMessages: [],\n flushQueuedMessages: async () => undefined,\n discardQueuedMessages: () => undefined,\n sendMessage: async () => 'failed',\n cancel: () => undefined,\n clearMessages: () => undefined,\n};\n\nlet snapshot: ChatLiveStreamSnapshot = EMPTY;\nconst listeners = new Set<() => void>();\n\nfunction publish(next: ChatLiveStreamSnapshot) {\n const prev = snapshot;\n if (\n prev.messages === next.messages &&\n prev.response === next.response &&\n prev.quickReply === next.quickReply &&\n prev.error === next.error &&\n prev.isLoading === next.isLoading &&\n prev.isStreaming === next.isStreaming &&\n prev.messagesLoading === next.messagesLoading &&\n prev.queuedMessages === next.queuedMessages &&\n prev.sendMessage === next.sendMessage\n ) {\n snapshot = next;\n return;\n }\n snapshot = next;\n listeners.forEach((fn) => fn());\n}\n\nfunction subscribe(listener: () => void) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nfunction getSnapshot() {\n return snapshot;\n}\n\n/** Keeps messenger subscriptions alive while the user is on history / home. */\nexport function ChatLiveStreamEngine() {\n const cdecli = useCdecliConnection();\n const routing: UseChatStreamRouting = useMemo(\n () => ({\n channelConnected: cdecli.channelConnected,\n accountId: cdecli.accountId,\n persistenceMode: cdecli.persistenceMode ?? 'backend',\n recoverAgentAuth: cdecli.recoverAgentAuth,\n }),\n [cdecli.channelConnected, cdecli.accountId, cdecli.persistenceMode, cdecli.recoverAgentAuth],\n );\n const stream = useChatStream(cdecli.activeChannelId ?? null, routing);\n useEffect(() => {\n publish(stream);\n }, [stream]);\n return null;\n}\n\nexport function useChatLiveStream(): ChatLiveStreamSnapshot {\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n"],"names":["_a"],"mappings":"gLAcA,MAAM,KAAgC,GAAA;AAAA,EACpC,UAAU,EAAC;AAAA,EACX,QAAU,EAAA,EAAA;AAAA,EACV,UAAY,EAAA,EAAA;AAAA,EACZ,KAAO,EAAA,IAAA;AAAA,EACP,SAAW,EAAA,KAAA;AAAA,EACX,WAAa,EAAA,KAAA;AAAA,EACb,eAAiB,EAAA,KAAA;AAAA,EACjB,WAAa,EAAA,KAAA;AAAA,EACb,gBAAgB,EAAC;AAAA,EACjB,qBAAqB,YAAY,MAAA;AAAA,EACjC,uBAAuB,MAAM,MAAA;AAAA,EAC7B,aAAa,YAAY,QAAA;AAAA,EACzB,QAAQ,MAAM,MAAA;AAAA,EACd,eAAe,MAAM;AACvB,CAAA;AACA,IAAI,QAAmC,GAAA,KAAA;AACvC,MAAM,SAAA,uBAAgB,GAAgB,EAAA;AACtC,SAAS,QAAQ,IAA8B,EAAA;AAC7C,EAAA,MAAM,IAAO,GAAA,QAAA;AACb,EAAA,IAAI,IAAK,CAAA,QAAA,KAAa,IAAK,CAAA,QAAA,IAAY,KAAK,QAAa,KAAA,IAAA,CAAK,QAAY,IAAA,IAAA,CAAK,eAAe,IAAK,CAAA,UAAA,IAAc,IAAK,CAAA,KAAA,KAAU,KAAK,KAAS,IAAA,IAAA,CAAK,SAAc,KAAA,IAAA,CAAK,aAAa,IAAK,CAAA,WAAA,KAAgB,IAAK,CAAA,WAAA,IAAe,KAAK,eAAoB,KAAA,IAAA,CAAK,eAAmB,IAAA,IAAA,CAAK,mBAAmB,IAAK,CAAA,cAAA,IAAkB,IAAK,CAAA,WAAA,KAAgB,KAAK,WAAa,EAAA;AACjW,IAAW,QAAA,GAAA,IAAA;AACX,IAAA;AAAA;AAEF,EAAW,QAAA,GAAA,IAAA;AACX,EAAU,SAAA,CAAA,OAAA,CAAQ,CAAM,EAAA,KAAA,EAAA,EAAI,CAAA;AAC9B;AACA,SAAS,UAAU,QAAsB,EAAA;AACvC,EAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,EAAA,OAAO,MAAM;AACX,IAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,GAC3B;AACF;AACA,SAAS,WAAc,GAAA;AACrB,EAAO,OAAA,QAAA;AACT;AAGO,SAAS,oBAAuB,GAAA;AApDvC,EAAA,IAAA,EAAA;AAqDE,EAAA,MAAM,SAAS,mBAAoB,EAAA;AACnC,EAAM,MAAA,OAAA,GAAgC,QAAQ,MAAG;AAtDnD,IAAAA,IAAAA,GAAAA;AAsDuD,IAAA,OAAA;AAAA,MACnD,kBAAkB,MAAO,CAAA,gBAAA;AAAA,MACzB,WAAW,MAAO,CAAA,SAAA;AAAA,MAClB,eAAiBA,EAAAA,CAAAA,GAAAA,GAAA,MAAO,CAAA,eAAA,KAAP,OAAAA,GAA0B,GAAA,SAAA;AAAA,MAC3C,kBAAkB,MAAO,CAAA;AAAA,KAC3B;AAAA,GAAI,EAAA,CAAC,OAAO,gBAAkB,EAAA,MAAA,CAAO,WAAW,MAAO,CAAA,eAAA,EAAiB,MAAO,CAAA,gBAAgB,CAAC,CAAA;AAChG,EAAA,MAAM,SAAS,aAAc,CAAA,CAAA,EAAA,GAAA,MAAA,CAAO,eAAP,KAAA,IAAA,GAAA,EAAA,GAA0B,MAAM,OAAO,CAAA;AACpE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,OAAA,CAAQ,MAAM,CAAA;AAAA,GAChB,EAAG,CAAC,MAAM,CAAC,CAAA;AACX,EAAO,OAAA,IAAA;AACT;AACO,SAAS,iBAA4C,GAAA;AAC1D,EAAO,OAAA,oBAAA,CAAqB,SAAW,EAAA,WAAA,EAAa,WAAW,CAAA;AACjE"}
@@ -54,6 +54,9 @@ function getCachedCanvasBoards(url) {
54
54
  if (!url || !boardsCache || boardsCache.url !== url) return null;
55
55
  return boardsCache.boards;
56
56
  }
57
+ function clearCachedCanvasBoards() {
58
+ boardsCache = null;
59
+ }
57
60
  function setCachedCanvasBoards(url, boards) {
58
61
  if (!url) return;
59
62
  boardsCache = {
@@ -151,4 +154,4 @@ function coerceMetadata(value) {
151
154
  } catch (e) {
152
155
  return null;
153
156
  }
154
- }export{fetchMyCanvasBoards,getCachedCanvasBoards,setCachedCanvasBoards,surfaceIndexUrl,surfaceIndexUrlFromTemplate};//# sourceMappingURL=surfaceIndex.js.map
157
+ }export{clearCachedCanvasBoards,fetchMyCanvasBoards,getCachedCanvasBoards,setCachedCanvasBoards,surfaceIndexUrl,surfaceIndexUrlFromTemplate};//# sourceMappingURL=surfaceIndex.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"surfaceIndex.js","sources":["../../../src/features/canvas/surfaceIndex.ts"],"sourcesContent":["/**\n * Native reader for the canvas boards in the SurfaceChannel index.\n *\n * The index (`servers/surface-index-subgraph-server`) is the ONLY store for a\n * canvas: the whole `PersistedCanvasState` rides in the row's `metadata`, and\n * `mySurfaceChannels` enumerates the caller's boards. Rows are per-USER, so the\n * phone — signed in as the same user — sees exactly what the web surface sees.\n *\n * This is the native twin of `@yantra/canvas-surface`'s `state/boards.ts`\n * (`pullBoards`). It is copied rather than imported for the same reason\n * `canvasCore.ts` is: that package builds for the DOM (vite, `import.meta.env`,\n * `window.location`), and importing it would drag the web toolchain into Metro.\n * The contract that matters is the wire shape, which both sides pin.\n *\n * Reads are best-effort: an unreachable index yields an error string for the UI\n * rather than throwing, matching how the web surface degrades.\n */\nimport { emptyBoardState, parseBoardState, type PersistedCanvasState } from './canvasCore';\n\n/** Fields the index returns for a channel row (mirror of CHANNEL_FIELDS). */\nconst CHANNEL_FIELDS = 'id surface channelId title metadata updatedAt';\n\n/**\n * The web surface's `/c/new` scratch board. It is not a real board (no host\n * session backs it), and `pullBoards` skips it — so must this, or an empty\n * scratch row would shadow the user's actual canvases.\n */\nconst SCRATCH_BOARD_ID = 'global';\n\nexport interface CanvasBoardSummary {\n channelId: string;\n title?: string;\n savedAt: number;\n itemCount: number;\n board: PersistedCanvasState;\n}\n\n/**\n * Where the index lives for this environment.\n *\n * Mirrors the surface-kit fallback: the index is a sibling host of the backend,\n * so drop the backend's first label and prefix `surface-index-backend.`\n * (`ideback.yantra-app-v1.cdebase.dev` -> `surface-index-backend.yantra-app-v1.cdebase.dev`).\n * An explicit URL always wins, so a non-standard environment sets one env var\n * instead of needing a code change.\n */\nexport function surfaceIndexUrl(graphqlUrl: string, explicit?: string): string {\n const override = (explicit || '').trim();\n if (override) return override;\n\n const base = (graphqlUrl || '').trim();\n if (!base) return '';\n try {\n const { protocol, host } = parseOrigin(base);\n const labels = host.split('.');\n if (labels.length < 3) return '';\n return `${protocol}//surface-index-backend.${labels.slice(1).join('.')}/graphql`;\n } catch {\n return '';\n }\n}\n\n/**\n * Index GraphQL URL from the same SURFACE_URL_TEMPLATE the native remotes use\n * (`https://{slug}-surface.<cluster>/` → `https://surface-index-backend.<cluster>/graphql`).\n * Local GRAPHQL_URL cannot derive a host; this is the develop-lane fallback.\n */\nexport function surfaceIndexUrlFromTemplate(template: string): string {\n const trimmed = (template || '').trim();\n const match = trimmed.match(/^(https?):\\/\\/\\{slug\\}-surface\\.([^/]+)/i);\n if (!match) return '';\n const host = match[2].replace(/\\/+$/, '');\n if (!host) return '';\n return `${match[1].toLowerCase()}://surface-index-backend.${host}/graphql`;\n}\n\nfunction parseOrigin(url: string): { protocol: string; host: string } {\n // RN has URL via react-native-url-polyfill (imported in the app entry), but\n // this module is also loaded by jest and the surface build, so parse by hand.\n const match = url.match(/^(https?:)\\/\\/([^/]+)/i);\n if (!match) throw new Error(`unparseable url: ${url}`);\n return { protocol: match[1].toLowerCase(), host: match[2].toLowerCase() };\n}\n\nexport interface FetchBoardsOptions {\n url: string;\n token?: string | null;\n signal?: AbortSignal;\n}\n\nexport interface FetchBoardsResult {\n boards: CanvasBoardSummary[];\n /** Plain sentence for the UI; null when the read succeeded. */\n error: string | null;\n}\n\n/** In-memory board list so revisiting Canvas Native does not re-flash the spinner. */\nlet boardsCache: { url: string; boards: CanvasBoardSummary[] } | null = null;\n\nexport function getCachedCanvasBoards(url: string): CanvasBoardSummary[] | null {\n if (!url || !boardsCache || boardsCache.url !== url) return null;\n return boardsCache.boards;\n}\n\nexport function setCachedCanvasBoards(url: string, boards: CanvasBoardSummary[]): void {\n if (!url) return;\n boardsCache = { url, boards };\n}\n\n/**\n * The caller's canvases, newest first.\n *\n * Empty boards are dropped, matching web `pullBoards` — the grid only shows\n * canvases with media. Only the scratch `global` row is skipped for the same\n * reason as web (it would merge every `/c/new` scratch into one shared row).\n */\nexport async function fetchMyCanvasBoards(opts: FetchBoardsOptions): Promise<FetchBoardsResult> {\n if (!opts.url) {\n return { boards: [], error: 'No canvas index configured for this build.' };\n }\n\n let payload: SurfaceIndexResponse;\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `query MySurfaceChannels($surface: SurfaceType) {\n mySurfaceChannels(surface: $surface) { ${CHANNEL_FIELDS} }\n }`,\n variables: { surface: 'canvas' },\n }),\n signal: opts.signal,\n });\n\n if (response.status === 401 || response.status === 403) {\n return { boards: [], error: 'Your session expired. Sign in again to see your canvases.' };\n }\n if (!response.ok) {\n return { boards: [], error: `The canvas index is unreachable (HTTP ${response.status}).` };\n }\n payload = (await response.json()) as SurfaceIndexResponse;\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n return { boards: [], error: null };\n }\n return { boards: [], error: 'Could not reach the canvas index. Check your connection.' };\n }\n\n if (payload?.errors?.length) {\n // GraphQL errors arrive with HTTP 200. The index reports an unauthenticated\n // caller as \"Missing tenantId in userContext\", which is a developer\n // sentence - say what the user can actually do about it.\n const raw = payload.errors[0]?.message || '';\n const isAuth = /tenantid|unauthenticated|unauthorized|not authorised|forbidden/i.test(raw);\n return {\n boards: [],\n error: isAuth\n ? 'Your session expired. Sign in again to see your canvases.'\n : raw || 'The canvas index rejected the request.',\n };\n }\n\n const rows = payload?.data?.mySurfaceChannels ?? [];\n const boards: CanvasBoardSummary[] = [];\n for (const row of rows) {\n const channelId = typeof row?.channelId === 'string' ? row.channelId : '';\n if (!channelId || channelId === SCRATCH_BOARD_ID) continue;\n const title = typeof row?.title === 'string' ? row.title : undefined;\n const board = parseBoardState(coerceMetadata(row?.metadata)) ?? emptyBoardState(title);\n if (board.items.length === 0) continue;\n boards.push({\n channelId,\n title: board.title || title,\n savedAt: board.savedAt,\n itemCount: board.items.length,\n board,\n });\n }\n\n boards.sort((a, b) => b.savedAt - a.savedAt);\n setCachedCanvasBoards(opts.url, boards);\n return { boards, error: null };\n}\n\nexport interface PushBoardOptions {\n url: string;\n token?: string | null;\n channelId: string;\n state: PersistedCanvasState;\n}\n\n/**\n * Write one board to the index. Twin of canvas-surface `pushBoard`:\n * `registerSurfaceChannel` is the idempotent upsert (update throws when the\n * row does not exist). Metadata is replaced wholesale — send the complete state.\n *\n * Returns false when the id is unsyncable (`global` / empty) or the write fails.\n * Never throws.\n */\nexport async function pushCanvasBoard(opts: PushBoardOptions): Promise<boolean> {\n const channelId = opts.channelId?.trim();\n if (!channelId || channelId === SCRATCH_BOARD_ID) return false;\n if (!opts.url) return false;\n\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `mutation RegisterSurfaceChannel($input: RegisterSurfaceChannelInput!) {\n registerSurfaceChannel(input: $input) { ${CHANNEL_FIELDS} }\n }`,\n variables: {\n input: {\n surface: 'canvas',\n channelId,\n sessionId: channelId,\n ...(opts.state.title ? { title: opts.state.title } : {}),\n metadata: opts.state,\n },\n },\n }),\n });\n if (!response.ok) return false;\n const payload = (await response.json()) as {\n data?: { registerSurfaceChannel?: { id?: string } | null };\n errors?: Array<{ message?: string }>;\n };\n if (payload?.errors?.length) {\n if (__DEV__) {\n // eslint-disable-next-line no-console\n console.warn('[pushCanvasBoard]', payload.errors[0]?.message);\n }\n return false;\n }\n return Boolean(payload?.data?.registerSurfaceChannel?.id);\n } catch (err) {\n if (__DEV__) {\n // eslint-disable-next-line no-console\n console.warn('[pushCanvasBoard] failed:', err);\n }\n return false;\n }\n}\n\n/**\n * `metadata` is a JSON scalar, which arrives decoded — but a backend that types\n * it as a String hands back the encoded text instead, and a board that silently\n * fails to parse looks exactly like an empty canvas. Accept both.\n */\nfunction coerceMetadata(value: unknown): unknown {\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\ninterface SurfaceIndexResponse {\n data?: { mySurfaceChannels?: Array<Record<string, unknown>> | null } | null;\n errors?: Array<{ message?: string }>;\n}\n\n/** Mint a board id the same way the web does: the board IS its channel. */\nexport function newBoardChannelId(): string {\n const rand = () => Math.random().toString(36).slice(2, 10);\n try {\n const bytes = new Uint8Array(8);\n (globalThis.crypto as Crypto | undefined)?.getRandomValues?.(bytes);\n if (bytes.some((b) => b !== 0)) {\n return `canvas-${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`;\n }\n } catch {\n // fall through to Math.random\n }\n return `canvas-${rand()}${rand()}`;\n}\n\n/**\n * Write one board to the index - the native twin of the web's `pushBoard`.\n * `registerSurfaceChannel` because it is the idempotent upsert (update throws\n * when the row does not exist). `metadata` is REPLACED wholesale by the\n * backend, so the complete state goes every time; never send a patch.\n */\nexport async function saveCanvasBoard(opts: {\n url: string;\n token?: string | null;\n channelId: string;\n board: PersistedCanvasState;\n}): Promise<{ ok: boolean; error: string | null }> {\n if (!opts.url) return { ok: false, error: 'No canvas index configured for this build.' };\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `mutation RegisterSurfaceChannel($input: RegisterSurfaceChannelInput!) {\n registerSurfaceChannel(input: $input) { id channelId }\n }`,\n variables: {\n input: {\n surface: 'canvas',\n channelId: opts.channelId,\n title: opts.board.title || 'Untitled canvas',\n metadata: opts.board,\n },\n },\n }),\n });\n if (!response.ok) return { ok: false, error: `Save failed (HTTP ${response.status}).` };\n const payload = (await response.json()) as { errors?: Array<{ message?: string }> };\n if (payload?.errors?.length) return { ok: false, error: payload.errors[0]?.message || 'Save rejected.' };\n return { ok: true, error: null };\n } catch {\n return { ok: false, error: 'Could not reach the canvas index to save.' };\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"surfaceIndex.js","sources":["../../../src/features/canvas/surfaceIndex.ts"],"sourcesContent":["/**\n * Native reader for the canvas boards in the SurfaceChannel index.\n *\n * The index (`servers/surface-index-subgraph-server`) is the ONLY store for a\n * canvas: the whole `PersistedCanvasState` rides in the row's `metadata`, and\n * `mySurfaceChannels` enumerates the caller's boards. Rows are per-USER, so the\n * phone — signed in as the same user — sees exactly what the web surface sees.\n *\n * This is the native twin of `@yantra/canvas-surface`'s `state/boards.ts`\n * (`pullBoards`). It is copied rather than imported for the same reason\n * `canvasCore.ts` is: that package builds for the DOM (vite, `import.meta.env`,\n * `window.location`), and importing it would drag the web toolchain into Metro.\n * The contract that matters is the wire shape, which both sides pin.\n *\n * Reads are best-effort: an unreachable index yields an error string for the UI\n * rather than throwing, matching how the web surface degrades.\n */\nimport { emptyBoardState, parseBoardState, type PersistedCanvasState } from './canvasCore';\n\n/** Fields the index returns for a channel row (mirror of CHANNEL_FIELDS). */\nconst CHANNEL_FIELDS = 'id surface channelId title metadata updatedAt';\n\n/**\n * The web surface's `/c/new` scratch board. It is not a real board (no host\n * session backs it), and `pullBoards` skips it — so must this, or an empty\n * scratch row would shadow the user's actual canvases.\n */\nconst SCRATCH_BOARD_ID = 'global';\n\nexport interface CanvasBoardSummary {\n channelId: string;\n title?: string;\n savedAt: number;\n itemCount: number;\n board: PersistedCanvasState;\n}\n\n/**\n * Where the index lives for this environment.\n *\n * Mirrors the surface-kit fallback: the index is a sibling host of the backend,\n * so drop the backend's first label and prefix `surface-index-backend.`\n * (`ideback.yantra-app-v1.cdebase.dev` -> `surface-index-backend.yantra-app-v1.cdebase.dev`).\n * An explicit URL always wins, so a non-standard environment sets one env var\n * instead of needing a code change.\n */\nexport function surfaceIndexUrl(graphqlUrl: string, explicit?: string): string {\n const override = (explicit || '').trim();\n if (override) return override;\n\n const base = (graphqlUrl || '').trim();\n if (!base) return '';\n try {\n const { protocol, host } = parseOrigin(base);\n const labels = host.split('.');\n if (labels.length < 3) return '';\n return `${protocol}//surface-index-backend.${labels.slice(1).join('.')}/graphql`;\n } catch {\n return '';\n }\n}\n\n/**\n * Index GraphQL URL from the same SURFACE_URL_TEMPLATE the native remotes use\n * (`https://{slug}-surface.<cluster>/` → `https://surface-index-backend.<cluster>/graphql`).\n * Local GRAPHQL_URL cannot derive a host; this is the develop-lane fallback.\n */\nexport function surfaceIndexUrlFromTemplate(template: string): string {\n const trimmed = (template || '').trim();\n const match = trimmed.match(/^(https?):\\/\\/\\{slug\\}-surface\\.([^/]+)/i);\n if (!match) return '';\n const host = match[2].replace(/\\/+$/, '');\n if (!host) return '';\n return `${match[1].toLowerCase()}://surface-index-backend.${host}/graphql`;\n}\n\nfunction parseOrigin(url: string): { protocol: string; host: string } {\n // RN has URL via react-native-url-polyfill (imported in the app entry), but\n // this module is also loaded by jest and the surface build, so parse by hand.\n const match = url.match(/^(https?:)\\/\\/([^/]+)/i);\n if (!match) throw new Error(`unparseable url: ${url}`);\n return { protocol: match[1].toLowerCase(), host: match[2].toLowerCase() };\n}\n\nexport interface FetchBoardsOptions {\n url: string;\n token?: string | null;\n signal?: AbortSignal;\n}\n\nexport interface FetchBoardsResult {\n boards: CanvasBoardSummary[];\n /** Plain sentence for the UI; null when the read succeeded. */\n error: string | null;\n}\n\n/** In-memory board list so revisiting Canvas Native does not re-flash the spinner. */\nlet boardsCache: { url: string; boards: CanvasBoardSummary[] } | null = null;\n\nexport function getCachedCanvasBoards(url: string): CanvasBoardSummary[] | null {\n if (!url || !boardsCache || boardsCache.url !== url) return null;\n return boardsCache.boards;\n}\n\n/**\n * Drop the in-memory board cache. Boards are re-fetchable from the surface\n * index, so this is safe to call under memory pressure - the next read simply\n * goes to the network. Registered as a memory reclaimer (see features/memory).\n */\nexport function clearCachedCanvasBoards(): void {\n boardsCache = null;\n}\n\nexport function setCachedCanvasBoards(url: string, boards: CanvasBoardSummary[]): void {\n if (!url) return;\n boardsCache = { url, boards };\n}\n\n/**\n * The caller's canvases, newest first.\n *\n * Empty boards are dropped, matching web `pullBoards` — the grid only shows\n * canvases with media. Only the scratch `global` row is skipped for the same\n * reason as web (it would merge every `/c/new` scratch into one shared row).\n */\nexport async function fetchMyCanvasBoards(opts: FetchBoardsOptions): Promise<FetchBoardsResult> {\n if (!opts.url) {\n return { boards: [], error: 'No canvas index configured for this build.' };\n }\n\n let payload: SurfaceIndexResponse;\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `query MySurfaceChannels($surface: SurfaceType) {\n mySurfaceChannels(surface: $surface) { ${CHANNEL_FIELDS} }\n }`,\n variables: { surface: 'canvas' },\n }),\n signal: opts.signal,\n });\n\n if (response.status === 401 || response.status === 403) {\n return { boards: [], error: 'Your session expired. Sign in again to see your canvases.' };\n }\n if (!response.ok) {\n return { boards: [], error: `The canvas index is unreachable (HTTP ${response.status}).` };\n }\n payload = (await response.json()) as SurfaceIndexResponse;\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n return { boards: [], error: null };\n }\n return { boards: [], error: 'Could not reach the canvas index. Check your connection.' };\n }\n\n if (payload?.errors?.length) {\n // GraphQL errors arrive with HTTP 200. The index reports an unauthenticated\n // caller as \"Missing tenantId in userContext\", which is a developer\n // sentence - say what the user can actually do about it.\n const raw = payload.errors[0]?.message || '';\n const isAuth = /tenantid|unauthenticated|unauthorized|not authorised|forbidden/i.test(raw);\n return {\n boards: [],\n error: isAuth\n ? 'Your session expired. Sign in again to see your canvases.'\n : raw || 'The canvas index rejected the request.',\n };\n }\n\n const rows = payload?.data?.mySurfaceChannels ?? [];\n const boards: CanvasBoardSummary[] = [];\n for (const row of rows) {\n const channelId = typeof row?.channelId === 'string' ? row.channelId : '';\n if (!channelId || channelId === SCRATCH_BOARD_ID) continue;\n const title = typeof row?.title === 'string' ? row.title : undefined;\n const board = parseBoardState(coerceMetadata(row?.metadata)) ?? emptyBoardState(title);\n if (board.items.length === 0) continue;\n boards.push({\n channelId,\n title: board.title || title,\n savedAt: board.savedAt,\n itemCount: board.items.length,\n board,\n });\n }\n\n boards.sort((a, b) => b.savedAt - a.savedAt);\n setCachedCanvasBoards(opts.url, boards);\n return { boards, error: null };\n}\n\nexport interface PushBoardOptions {\n url: string;\n token?: string | null;\n channelId: string;\n state: PersistedCanvasState;\n}\n\n/**\n * Write one board to the index. Twin of canvas-surface `pushBoard`:\n * `registerSurfaceChannel` is the idempotent upsert (update throws when the\n * row does not exist). Metadata is replaced wholesale — send the complete state.\n *\n * Returns false when the id is unsyncable (`global` / empty) or the write fails.\n * Never throws.\n */\nexport async function pushCanvasBoard(opts: PushBoardOptions): Promise<boolean> {\n const channelId = opts.channelId?.trim();\n if (!channelId || channelId === SCRATCH_BOARD_ID) return false;\n if (!opts.url) return false;\n\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `mutation RegisterSurfaceChannel($input: RegisterSurfaceChannelInput!) {\n registerSurfaceChannel(input: $input) { ${CHANNEL_FIELDS} }\n }`,\n variables: {\n input: {\n surface: 'canvas',\n channelId,\n sessionId: channelId,\n ...(opts.state.title ? { title: opts.state.title } : {}),\n metadata: opts.state,\n },\n },\n }),\n });\n if (!response.ok) return false;\n const payload = (await response.json()) as {\n data?: { registerSurfaceChannel?: { id?: string } | null };\n errors?: Array<{ message?: string }>;\n };\n if (payload?.errors?.length) {\n if (__DEV__) {\n // eslint-disable-next-line no-console\n console.warn('[pushCanvasBoard]', payload.errors[0]?.message);\n }\n return false;\n }\n return Boolean(payload?.data?.registerSurfaceChannel?.id);\n } catch (err) {\n if (__DEV__) {\n // eslint-disable-next-line no-console\n console.warn('[pushCanvasBoard] failed:', err);\n }\n return false;\n }\n}\n\n/**\n * `metadata` is a JSON scalar, which arrives decoded — but a backend that types\n * it as a String hands back the encoded text instead, and a board that silently\n * fails to parse looks exactly like an empty canvas. Accept both.\n */\nfunction coerceMetadata(value: unknown): unknown {\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\ninterface SurfaceIndexResponse {\n data?: { mySurfaceChannels?: Array<Record<string, unknown>> | null } | null;\n errors?: Array<{ message?: string }>;\n}\n\n/** Mint a board id the same way the web does: the board IS its channel. */\nexport function newBoardChannelId(): string {\n const rand = () => Math.random().toString(36).slice(2, 10);\n try {\n const bytes = new Uint8Array(8);\n (globalThis.crypto as Crypto | undefined)?.getRandomValues?.(bytes);\n if (bytes.some((b) => b !== 0)) {\n return `canvas-${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`;\n }\n } catch {\n // fall through to Math.random\n }\n return `canvas-${rand()}${rand()}`;\n}\n\n/**\n * Write one board to the index - the native twin of the web's `pushBoard`.\n * `registerSurfaceChannel` because it is the idempotent upsert (update throws\n * when the row does not exist). `metadata` is REPLACED wholesale by the\n * backend, so the complete state goes every time; never send a patch.\n */\nexport async function saveCanvasBoard(opts: {\n url: string;\n token?: string | null;\n channelId: string;\n board: PersistedCanvasState;\n}): Promise<{ ok: boolean; error: string | null }> {\n if (!opts.url) return { ok: false, error: 'No canvas index configured for this build.' };\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `mutation RegisterSurfaceChannel($input: RegisterSurfaceChannelInput!) {\n registerSurfaceChannel(input: $input) { id channelId }\n }`,\n variables: {\n input: {\n surface: 'canvas',\n channelId: opts.channelId,\n title: opts.board.title || 'Untitled canvas',\n metadata: opts.board,\n },\n },\n }),\n });\n if (!response.ok) return { ok: false, error: `Save failed (HTTP ${response.status}).` };\n const payload = (await response.json()) as { errors?: Array<{ message?: string }> };\n if (payload?.errors?.length) return { ok: false, error: payload.errors[0]?.message || 'Save rejected.' };\n return { ok: true, error: null };\n } catch {\n return { ok: false, error: 'Could not reach the canvas index to save.' };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import {jsx,Fragment,jsxs}from'react/jsx-runtime';import {useRef,useMemo,useCallback,useEffect,memo}from'react';import {View,StyleSheet,FlatList,Platform,Pressable}from'react-native';import {dismissKeyboard}from'../../utils/keyboardController.js';import {useKeyboardLiftHeight}from'../../components/KeyboardComposerDock.js';import Markdown from'react-native-markdown-display';import {shouldRenderUserTranscriptTurn,displayUserMessageText}from'../attachments/displayUserMessageText.js';import {MessageAttachmentPreviews}from'../attachments/MessageAttachmentPreviews.js';import {ToolTimeline}from'./ToolTimeline.js';import {parseToolActivity}from'./toolActivity.js';import {parseAskUser,displayAskUserAssistantText,stripAskUser}from'./askUser.js';import {collapseDuplicateUserRows,groupConsecutiveRoleRows}from'./transcriptGroups.js';import {AssistantMarkdown}from'./AssistantMarkdown.js';var __defProp = Object.defineProperty;
1
+ import {jsx,Fragment,jsxs}from'react/jsx-runtime';import {useRef,useMemo,useCallback,useEffect,memo}from'react';import {View,StyleSheet,FlatList,Platform,Pressable,Text}from'react-native';import {dismissKeyboard}from'../../utils/keyboardController.js';import {useKeyboardLiftHeight}from'../../components/KeyboardComposerDock.js';import Markdown from'react-native-markdown-display';import {shouldRenderUserTranscriptTurn,displayUserMessageText}from'../attachments/displayUserMessageText.js';import {MessageAttachmentPreviews}from'../attachments/MessageAttachmentPreviews.js';import {ToolTimeline}from'./ToolTimeline.js';import {parseToolActivity}from'./toolActivity.js';import {parseAskUser,displayAskUserAssistantText,stripAskUser}from'./askUser.js';import {collapseDuplicateUserRows,groupConsecutiveRoleRows}from'./transcriptGroups.js';import {AssistantMarkdown}from'./AssistantMarkdown.js';var __defProp = Object.defineProperty;
2
2
  var __defProps = Object.defineProperties;
3
3
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -160,7 +160,10 @@ const TranscriptGroupView = memo(function TranscriptGroupView2({
160
160
  if (!text && attachments.length === 0) return null;
161
161
  return /* @__PURE__ */ jsxs(View, { style: [styles.userTurn, index > 0 && styles.userFollow], children: [
162
162
  attachments.length > 0 ? /* @__PURE__ */ jsx(MessageAttachmentPreviews, { attachments, align: "end", isDark }) : null,
163
- text ? /* @__PURE__ */ jsx(View, { style: styles.userBubble, children: /* @__PURE__ */ jsx(Markdown, { style: USER_MARKDOWN, children: text }) }) : null
163
+ text ? /* @__PURE__ */ jsx(View, { style: [styles.userBubble, item.queued && styles.userBubbleQueued], children: /* @__PURE__ */ jsx(Markdown, { style: USER_MARKDOWN, children: text }) }) : null,
164
+ item.queued ? /* @__PURE__ */ jsx(Text, { style: [styles.queuedCaption, {
165
+ color: isDark ? "#94a3b8" : "#64748b"
166
+ }], children: "Queued \xB7 sends when the gateway connects" }) : null
164
167
  ] }, item.id);
165
168
  }) });
166
169
  }
@@ -253,6 +256,16 @@ const styles = StyleSheet.create({
253
256
  paddingHorizontal: 12,
254
257
  paddingVertical: 8
255
258
  },
259
+ // Same bubble, held back: a queued turn reads as pending, not as delivered.
260
+ userBubbleQueued: {
261
+ opacity: 0.55
262
+ },
263
+ queuedCaption: {
264
+ fontSize: 11,
265
+ fontWeight: "500",
266
+ marginTop: 4,
267
+ marginRight: 2
268
+ },
256
269
  assistantGroup: {
257
270
  width: "100%",
258
271
  marginBottom: 4
@@ -1 +1 @@
1
- {"version":3,"file":"ChatTranscript.js","sources":["../../../src/features/chat/ChatTranscript.tsx"],"sourcesContent":["/**\n * Chat thread list that can show attachment previews.\n *\n * `@messenger-box/platform-mobile` PlanModeView maps each row to GiftedChat\n * `{ text }` only, so an image send becomes the gateway caption\n * \"Attached: IMG_0005.jpg\" with no thumbnail. This list keeps streaming\n * markdown for the assistant and draws Manus-style previews on the user side.\n *\n * Rows stay oldest → newest (same as web). A previous `inverted` FlatList\n * reversed that order on device. The composer dock already lifts with the\n * keyboard; this list just fills the leftover space and pins to the latest\n * turn.\n */\nimport React, { memo, useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react';\nimport { FlatList, Platform, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { dismissKeyboard } from '../../utils/keyboardController';\nimport { useKeyboardLiftHeight } from '../../components/KeyboardComposerDock';\nimport Markdown from 'react-native-markdown-display';\nimport type { MessageAttachment } from '../../hooks/useChatStream';\nimport { displayUserMessageText, shouldRenderUserTranscriptTurn } from '../attachments/displayUserMessageText';\nimport { MessageAttachmentPreviews } from '../attachments/MessageAttachmentPreviews';\nimport { ToolTimeline } from './ToolTimeline';\nimport { parseToolActivity } from './toolActivity';\nimport { parseAskUser, stripAskUser, displayAskUserAssistantText } from './askUser';\nimport { collapseDuplicateUserRows, groupConsecutiveRoleRows, type RoleGroup } from './transcriptGroups';\nimport { AssistantMarkdown } from './AssistantMarkdown';\n\nexport type TranscriptMessage = {\n id: string;\n role: string;\n content: string;\n displayContent?: string;\n attachments?: MessageAttachment[];\n};\n\ntype TranscriptRow = TranscriptMessage & { streaming?: boolean; askAnswered?: boolean };\ntype TranscriptGroup = RoleGroup<TranscriptRow>;\n\nconst USER_BUBBLE_BG = '#2563eb';\n\nexport function ChatTranscript({\n messages,\n streamingContent,\n renderMessageActions,\n listContentStyle,\n listFooter,\n isDark = false,\n}: {\n messages: TranscriptMessage[];\n streamingContent?: string;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n listContentStyle?: StyleProp<ViewStyle>;\n listFooter?: ReactNode;\n isDark?: boolean;\n}) {\n const listRef = useRef<FlatList<TranscriptGroup>>(null);\n const stickToBottomRef = useRef(true);\n const isProgrammaticScrollRef = useRef(false);\n const ignoreScrollUntilRef = useRef(0);\n const keyboardHeight = useKeyboardLiftHeight();\n\n const historyRows = useMemo<TranscriptRow[]>(() => {\n const rows: TranscriptRow[] = [];\n messages.forEach((msg, index) => {\n if (msg.role === 'user') {\n if (shouldRenderUserTranscriptTurn(msg.content, msg.attachments)) {\n rows.push({ ...msg, streaming: false });\n }\n return;\n }\n if (msg.role !== 'assistant') {\n rows.push({ ...msg, streaming: false });\n return;\n }\n const askAnswered =\n Boolean(parseAskUser(msg.content)) && messages.slice(index + 1).some((m) => m.role === 'user');\n const visible = displayAskUserAssistantText(msg.content, askAnswered).trim();\n const tools = parseToolActivity(stripAskUser(msg.content), false);\n const hasAsk = Boolean(parseAskUser(msg.content));\n if (hasAsk && !visible && tools.steps.length === 0 && !(msg.attachments?.length ?? 0)) {\n return;\n }\n if (!hasAsk) {\n const leftover = stripAskUser(msg.content).trim();\n if (!leftover && !(msg.attachments?.length ?? 0)) return;\n }\n rows.push({ ...msg, streaming: false, askAnswered });\n });\n return rows;\n }, [messages]);\n\n const rows = useMemo<TranscriptRow[]>(() => {\n const history = collapseDuplicateUserRows(historyRows);\n const rawStream = streamingContent ?? '';\n const last = history[history.length - 1];\n const streamAskAnswered = last?.role === 'user' && Boolean(parseAskUser(rawStream));\n const streamProse = displayAskUserAssistantText(rawStream, streamAskAnswered).trim();\n const streamTools = parseToolActivity(stripAskUser(rawStream), true);\n if (!streamProse && streamTools.steps.length === 0) return history;\n if (last?.role === 'assistant') {\n const lastText = last.content.trim();\n const streamText = rawStream.trim();\n if (lastText === streamText || lastText.startsWith(streamText) || streamText.startsWith(lastText)) {\n return history;\n }\n }\n return [\n ...history,\n {\n id: '__streaming__',\n role: 'assistant',\n content: rawStream,\n streaming: true,\n askAnswered: streamAskAnswered,\n },\n ];\n }, [historyRows, streamingContent]);\n\n const groups = useMemo(() => groupConsecutiveRoleRows(rows), [rows]);\n\n const scrollToLatest = useCallback(\n (animated: boolean) => {\n if (groups.length === 0) return;\n isProgrammaticScrollRef.current = true;\n stickToBottomRef.current = true;\n const list = listRef.current;\n if (!list) return;\n list.scrollToEnd({ animated });\n },\n [groups.length],\n );\n\n useEffect(() => {\n if (groups.length === 0 || !stickToBottomRef.current) return undefined;\n const t = setTimeout(() => scrollToLatest(false), 0);\n return () => clearTimeout(t);\n }, [groups.length, streamingContent?.length, scrollToLatest]);\n\n // Spacer and list height update on the same keyboard frame. Scroll only\n // after that shrink, or we stay on the top of a long turn (image 1).\n useEffect(() => {\n if (keyboardHeight <= 0) return undefined;\n stickToBottomRef.current = true;\n ignoreScrollUntilRef.current = Date.now() + 400;\n const frame = requestAnimationFrame(() => scrollToLatest(false));\n const afterLayout = setTimeout(() => scrollToLatest(false), 64);\n const afterKeyboard = setTimeout(() => scrollToLatest(false), 280);\n return () => {\n cancelAnimationFrame(frame);\n clearTimeout(afterLayout);\n clearTimeout(afterKeyboard);\n };\n }, [keyboardHeight, scrollToLatest]);\n\n const handleScroll = useCallback(\n (e: {\n nativeEvent: {\n contentOffset: { y: number };\n contentSize: { height: number };\n layoutMeasurement: { height: number };\n };\n }) => {\n if (Date.now() < ignoreScrollUntilRef.current) {\n stickToBottomRef.current = true;\n return;\n }\n const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;\n const nearLatest = contentOffset.y + layoutMeasurement.height >= contentSize.height - 80;\n if (!isProgrammaticScrollRef.current) {\n stickToBottomRef.current = nearLatest;\n } else if (nearLatest) {\n isProgrammaticScrollRef.current = false;\n }\n },\n [],\n );\n\n const handleContentSizeChange = useCallback(() => {\n if (!stickToBottomRef.current) return;\n scrollToLatest(false);\n }, [scrollToLatest]);\n\n const renderItem = useCallback(\n ({ item }: { item: TranscriptGroup }) => (\n <TranscriptGroupView group={item} isDark={isDark} renderMessageActions={renderMessageActions} />\n ),\n [isDark, renderMessageActions],\n );\n\n return (\n <View style={styles.list}>\n <FlatList\n ref={listRef}\n data={groups}\n keyExtractor={(item) => item.id}\n renderItem={renderItem}\n extraData={`${streamingContent ?? ''}:${Boolean(listFooter)}`}\n ListFooterComponent={listFooter ? <>{listFooter}</> : null}\n initialNumToRender={12}\n maxToRenderPerBatch={8}\n updateCellsBatchingPeriod={50}\n windowSize={21}\n scrollEnabled\n nestedScrollEnabled={false}\n directionalLockEnabled\n onScroll={handleScroll}\n onContentSizeChange={handleContentSizeChange}\n onLayout={() => {\n if (stickToBottomRef.current) scrollToLatest(false);\n }}\n scrollEventThrottle={16}\n keyboardShouldPersistTaps=\"handled\"\n keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}\n automaticallyAdjustKeyboardInsets={false}\n automaticallyAdjustContentInsets={false}\n contentInsetAdjustmentBehavior=\"never\"\n contentContainerStyle={[styles.listContent, listContentStyle, !listFooter && styles.listContentFill]}\n style={styles.list}\n />\n </View>\n );\n}\n\nconst TranscriptGroupView = memo(function TranscriptGroupView({\n group,\n isDark,\n renderMessageActions,\n}: {\n group: TranscriptGroup;\n isDark: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n if (group.role === 'user') {\n return (\n <Pressable style={styles.userCol} onPress={dismissKeyboard} accessible={false}>\n {group.items.map((item, index) => {\n const attachments = item.attachments ?? [];\n const text = displayUserMessageText(item.displayContent ?? item.content, attachments);\n if (!text && attachments.length === 0) return null;\n return (\n <View key={item.id} style={[styles.userTurn, index > 0 && styles.userFollow]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"end\" isDark={isDark} />\n ) : null}\n {text ? (\n <View style={styles.userBubble}>\n <Markdown style={USER_MARKDOWN}>{text}</Markdown>\n </View>\n ) : null}\n </View>\n );\n })}\n </Pressable>\n );\n }\n\n const last = group.items[group.items.length - 1];\n\n return (\n <View style={styles.assistantGroup}>\n {group.items.map((item, index) => (\n <AssistantTurnView\n key={item.id}\n item={item}\n isDark={isDark}\n tightTop={index > 0}\n renderMessageActions={item.streaming || item !== last ? undefined : renderMessageActions}\n />\n ))}\n </View>\n );\n});\n\nconst AssistantTurnView = memo(function AssistantTurnView({\n item,\n isDark,\n tightTop,\n renderMessageActions,\n}: {\n item: TranscriptRow;\n isDark: boolean;\n tightTop: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n const attachments = item.attachments ?? [];\n const stripped = stripAskUser(item.content);\n const toolActivity = parseToolActivity(stripped, !!item.streaming);\n const text = parseToolActivity(\n displayAskUserAssistantText(item.content, !!item.askAnswered),\n !!item.streaming,\n ).text;\n\n return (\n <View style={[styles.assistantCol, tightTop && styles.assistantColTight]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"start\" isDark={isDark} />\n ) : null}\n {toolActivity.steps.length > 0 ? (\n <ToolTimeline steps={toolActivity.steps} active={!!item.streaming} isDark={isDark} />\n ) : null}\n {text ? <AssistantMarkdown text={text} streaming={!!item.streaming} isDark={isDark} /> : null}\n {renderMessageActions\n ? renderMessageActions({ id: item.id, role: 'assistant', content: item.content })\n : null}\n </View>\n );\n});\n\nconst USER_MARKDOWN = {\n body: { color: '#ffffff', fontSize: 15, lineHeight: 22 },\n paragraph: { marginTop: 4, marginBottom: 4, color: '#ffffff' },\n strong: { fontWeight: '700' as const, color: '#ffffff' },\n em: { color: '#ffffff' },\n text: { color: '#ffffff' },\n bullet_list: { marginVertical: 4 },\n ordered_list: { marginVertical: 4 },\n list_item: { marginVertical: 2, color: '#ffffff' },\n};\n\nconst styles = StyleSheet.create({\n list: {\n flex: 1,\n minHeight: 0,\n },\n listContent: {\n paddingTop: 8,\n paddingBottom: 8,\n paddingHorizontal: 8,\n },\n listContentFill: {\n flexGrow: 1,\n },\n userCol: {\n width: '100%',\n alignItems: 'flex-end',\n marginBottom: 12,\n paddingHorizontal: 4,\n },\n userTurn: {\n maxWidth: '92%',\n alignItems: 'flex-end',\n },\n userFollow: {\n marginTop: 4,\n },\n userBubble: {\n backgroundColor: USER_BUBBLE_BG,\n borderRadius: 16,\n paddingHorizontal: 12,\n paddingVertical: 8,\n },\n assistantGroup: {\n width: '100%',\n marginBottom: 4,\n },\n assistantCol: {\n width: '100%',\n flexGrow: 0,\n alignItems: 'stretch',\n marginBottom: 8,\n paddingHorizontal: 4,\n },\n assistantColTight: {\n marginTop: -4,\n },\n});\n"],"names":["rows","TranscriptGroupView","AssistantTurnView"],"mappings":"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"}
1
+ {"version":3,"file":"ChatTranscript.js","sources":["../../../src/features/chat/ChatTranscript.tsx"],"sourcesContent":["/**\n * Chat thread list that can show attachment previews.\n *\n * `@messenger-box/platform-mobile` PlanModeView maps each row to GiftedChat\n * `{ text }` only, so an image send becomes the gateway caption\n * \"Attached: IMG_0005.jpg\" with no thumbnail. This list keeps streaming\n * markdown for the assistant and draws Manus-style previews on the user side.\n *\n * Rows stay oldest → newest (same as web). A previous `inverted` FlatList\n * reversed that order on device. The composer dock already lifts with the\n * keyboard; this list just fills the leftover space and pins to the latest\n * turn.\n */\nimport React, { memo, useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react';\nimport { FlatList, Platform, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { dismissKeyboard } from '../../utils/keyboardController';\nimport { useKeyboardLiftHeight } from '../../components/KeyboardComposerDock';\nimport Markdown from 'react-native-markdown-display';\nimport type { MessageAttachment } from '../../hooks/useChatStream';\nimport { displayUserMessageText, shouldRenderUserTranscriptTurn } from '../attachments/displayUserMessageText';\nimport { MessageAttachmentPreviews } from '../attachments/MessageAttachmentPreviews';\nimport { ToolTimeline } from './ToolTimeline';\nimport { parseToolActivity } from './toolActivity';\nimport { parseAskUser, stripAskUser, displayAskUserAssistantText } from './askUser';\nimport { collapseDuplicateUserRows, groupConsecutiveRoleRows, type RoleGroup } from './transcriptGroups';\nimport { AssistantMarkdown } from './AssistantMarkdown';\n\nexport type TranscriptMessage = {\n id: string;\n role: string;\n content: string;\n displayContent?: string;\n attachments?: MessageAttachment[];\n /** Typed while the gateway was down: shown in place, waiting on the outbox. */\n queued?: boolean;\n};\n\ntype TranscriptRow = TranscriptMessage & { streaming?: boolean; askAnswered?: boolean };\ntype TranscriptGroup = RoleGroup<TranscriptRow>;\n\nconst USER_BUBBLE_BG = '#2563eb';\n\nexport function ChatTranscript({\n messages,\n streamingContent,\n renderMessageActions,\n listContentStyle,\n listFooter,\n isDark = false,\n}: {\n messages: TranscriptMessage[];\n streamingContent?: string;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n listContentStyle?: StyleProp<ViewStyle>;\n listFooter?: ReactNode;\n isDark?: boolean;\n}) {\n const listRef = useRef<FlatList<TranscriptGroup>>(null);\n const stickToBottomRef = useRef(true);\n const isProgrammaticScrollRef = useRef(false);\n const ignoreScrollUntilRef = useRef(0);\n const keyboardHeight = useKeyboardLiftHeight();\n\n const historyRows = useMemo<TranscriptRow[]>(() => {\n const rows: TranscriptRow[] = [];\n messages.forEach((msg, index) => {\n if (msg.role === 'user') {\n if (shouldRenderUserTranscriptTurn(msg.content, msg.attachments)) {\n rows.push({ ...msg, streaming: false });\n }\n return;\n }\n if (msg.role !== 'assistant') {\n rows.push({ ...msg, streaming: false });\n return;\n }\n const askAnswered =\n Boolean(parseAskUser(msg.content)) && messages.slice(index + 1).some((m) => m.role === 'user');\n const visible = displayAskUserAssistantText(msg.content, askAnswered).trim();\n const tools = parseToolActivity(stripAskUser(msg.content), false);\n const hasAsk = Boolean(parseAskUser(msg.content));\n if (hasAsk && !visible && tools.steps.length === 0 && !(msg.attachments?.length ?? 0)) {\n return;\n }\n if (!hasAsk) {\n const leftover = stripAskUser(msg.content).trim();\n if (!leftover && !(msg.attachments?.length ?? 0)) return;\n }\n rows.push({ ...msg, streaming: false, askAnswered });\n });\n return rows;\n }, [messages]);\n\n const rows = useMemo<TranscriptRow[]>(() => {\n const history = collapseDuplicateUserRows(historyRows);\n const rawStream = streamingContent ?? '';\n const last = history[history.length - 1];\n const streamAskAnswered = last?.role === 'user' && Boolean(parseAskUser(rawStream));\n const streamProse = displayAskUserAssistantText(rawStream, streamAskAnswered).trim();\n const streamTools = parseToolActivity(stripAskUser(rawStream), true);\n if (!streamProse && streamTools.steps.length === 0) return history;\n if (last?.role === 'assistant') {\n const lastText = last.content.trim();\n const streamText = rawStream.trim();\n if (lastText === streamText || lastText.startsWith(streamText) || streamText.startsWith(lastText)) {\n return history;\n }\n }\n return [\n ...history,\n {\n id: '__streaming__',\n role: 'assistant',\n content: rawStream,\n streaming: true,\n askAnswered: streamAskAnswered,\n },\n ];\n }, [historyRows, streamingContent]);\n\n const groups = useMemo(() => groupConsecutiveRoleRows(rows), [rows]);\n\n const scrollToLatest = useCallback(\n (animated: boolean) => {\n if (groups.length === 0) return;\n isProgrammaticScrollRef.current = true;\n stickToBottomRef.current = true;\n const list = listRef.current;\n if (!list) return;\n list.scrollToEnd({ animated });\n },\n [groups.length],\n );\n\n useEffect(() => {\n if (groups.length === 0 || !stickToBottomRef.current) return undefined;\n const t = setTimeout(() => scrollToLatest(false), 0);\n return () => clearTimeout(t);\n }, [groups.length, streamingContent?.length, scrollToLatest]);\n\n // Spacer and list height update on the same keyboard frame. Scroll only\n // after that shrink, or we stay on the top of a long turn (image 1).\n useEffect(() => {\n if (keyboardHeight <= 0) return undefined;\n stickToBottomRef.current = true;\n ignoreScrollUntilRef.current = Date.now() + 400;\n const frame = requestAnimationFrame(() => scrollToLatest(false));\n const afterLayout = setTimeout(() => scrollToLatest(false), 64);\n const afterKeyboard = setTimeout(() => scrollToLatest(false), 280);\n return () => {\n cancelAnimationFrame(frame);\n clearTimeout(afterLayout);\n clearTimeout(afterKeyboard);\n };\n }, [keyboardHeight, scrollToLatest]);\n\n const handleScroll = useCallback(\n (e: {\n nativeEvent: {\n contentOffset: { y: number };\n contentSize: { height: number };\n layoutMeasurement: { height: number };\n };\n }) => {\n if (Date.now() < ignoreScrollUntilRef.current) {\n stickToBottomRef.current = true;\n return;\n }\n const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;\n const nearLatest = contentOffset.y + layoutMeasurement.height >= contentSize.height - 80;\n if (!isProgrammaticScrollRef.current) {\n stickToBottomRef.current = nearLatest;\n } else if (nearLatest) {\n isProgrammaticScrollRef.current = false;\n }\n },\n [],\n );\n\n const handleContentSizeChange = useCallback(() => {\n if (!stickToBottomRef.current) return;\n scrollToLatest(false);\n }, [scrollToLatest]);\n\n const renderItem = useCallback(\n ({ item }: { item: TranscriptGroup }) => (\n <TranscriptGroupView group={item} isDark={isDark} renderMessageActions={renderMessageActions} />\n ),\n [isDark, renderMessageActions],\n );\n\n return (\n <View style={styles.list}>\n <FlatList\n ref={listRef}\n data={groups}\n keyExtractor={(item) => item.id}\n renderItem={renderItem}\n extraData={`${streamingContent ?? ''}:${Boolean(listFooter)}`}\n ListFooterComponent={listFooter ? <>{listFooter}</> : null}\n initialNumToRender={12}\n maxToRenderPerBatch={8}\n updateCellsBatchingPeriod={50}\n windowSize={21}\n scrollEnabled\n nestedScrollEnabled={false}\n directionalLockEnabled\n onScroll={handleScroll}\n onContentSizeChange={handleContentSizeChange}\n onLayout={() => {\n if (stickToBottomRef.current) scrollToLatest(false);\n }}\n scrollEventThrottle={16}\n keyboardShouldPersistTaps=\"handled\"\n keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}\n automaticallyAdjustKeyboardInsets={false}\n automaticallyAdjustContentInsets={false}\n contentInsetAdjustmentBehavior=\"never\"\n contentContainerStyle={[styles.listContent, listContentStyle, !listFooter && styles.listContentFill]}\n style={styles.list}\n />\n </View>\n );\n}\n\nconst TranscriptGroupView = memo(function TranscriptGroupView({\n group,\n isDark,\n renderMessageActions,\n}: {\n group: TranscriptGroup;\n isDark: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n if (group.role === 'user') {\n return (\n <Pressable style={styles.userCol} onPress={dismissKeyboard} accessible={false}>\n {group.items.map((item, index) => {\n const attachments = item.attachments ?? [];\n const text = displayUserMessageText(item.displayContent ?? item.content, attachments);\n if (!text && attachments.length === 0) return null;\n return (\n <View key={item.id} style={[styles.userTurn, index > 0 && styles.userFollow]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"end\" isDark={isDark} />\n ) : null}\n {text ? (\n <View style={[styles.userBubble, item.queued && styles.userBubbleQueued]}>\n <Markdown style={USER_MARKDOWN}>{text}</Markdown>\n </View>\n ) : null}\n {item.queued ? (\n <Text style={[styles.queuedCaption, { color: isDark ? '#94a3b8' : '#64748b' }]}>\n Queued · sends when the gateway connects\n </Text>\n ) : null}\n </View>\n );\n })}\n </Pressable>\n );\n }\n\n const last = group.items[group.items.length - 1];\n\n return (\n <View style={styles.assistantGroup}>\n {group.items.map((item, index) => (\n <AssistantTurnView\n key={item.id}\n item={item}\n isDark={isDark}\n tightTop={index > 0}\n renderMessageActions={item.streaming || item !== last ? undefined : renderMessageActions}\n />\n ))}\n </View>\n );\n});\n\nconst AssistantTurnView = memo(function AssistantTurnView({\n item,\n isDark,\n tightTop,\n renderMessageActions,\n}: {\n item: TranscriptRow;\n isDark: boolean;\n tightTop: boolean;\n renderMessageActions?: (message: { id: string; role: string; content: string }) => React.ReactNode;\n}) {\n const attachments = item.attachments ?? [];\n const stripped = stripAskUser(item.content);\n const toolActivity = parseToolActivity(stripped, !!item.streaming);\n const text = parseToolActivity(\n displayAskUserAssistantText(item.content, !!item.askAnswered),\n !!item.streaming,\n ).text;\n\n return (\n <View style={[styles.assistantCol, tightTop && styles.assistantColTight]}>\n {attachments.length > 0 ? (\n <MessageAttachmentPreviews attachments={attachments} align=\"start\" isDark={isDark} />\n ) : null}\n {toolActivity.steps.length > 0 ? (\n <ToolTimeline steps={toolActivity.steps} active={!!item.streaming} isDark={isDark} />\n ) : null}\n {text ? <AssistantMarkdown text={text} streaming={!!item.streaming} isDark={isDark} /> : null}\n {renderMessageActions\n ? renderMessageActions({ id: item.id, role: 'assistant', content: item.content })\n : null}\n </View>\n );\n});\n\nconst USER_MARKDOWN = {\n body: { color: '#ffffff', fontSize: 15, lineHeight: 22 },\n paragraph: { marginTop: 4, marginBottom: 4, color: '#ffffff' },\n strong: { fontWeight: '700' as const, color: '#ffffff' },\n em: { color: '#ffffff' },\n text: { color: '#ffffff' },\n bullet_list: { marginVertical: 4 },\n ordered_list: { marginVertical: 4 },\n list_item: { marginVertical: 2, color: '#ffffff' },\n};\n\nconst styles = StyleSheet.create({\n list: {\n flex: 1,\n minHeight: 0,\n },\n listContent: {\n paddingTop: 8,\n paddingBottom: 8,\n paddingHorizontal: 8,\n },\n listContentFill: {\n flexGrow: 1,\n },\n userCol: {\n width: '100%',\n alignItems: 'flex-end',\n marginBottom: 12,\n paddingHorizontal: 4,\n },\n userTurn: {\n maxWidth: '92%',\n alignItems: 'flex-end',\n },\n userFollow: {\n marginTop: 4,\n },\n userBubble: {\n backgroundColor: USER_BUBBLE_BG,\n borderRadius: 16,\n paddingHorizontal: 12,\n paddingVertical: 8,\n },\n // Same bubble, held back: a queued turn reads as pending, not as delivered.\n userBubbleQueued: {\n opacity: 0.55,\n },\n queuedCaption: {\n fontSize: 11,\n fontWeight: '500',\n marginTop: 4,\n marginRight: 2,\n },\n assistantGroup: {\n width: '100%',\n marginBottom: 4,\n },\n assistantCol: {\n width: '100%',\n flexGrow: 0,\n alignItems: 'stretch',\n marginBottom: 8,\n paddingHorizontal: 4,\n },\n assistantColTight: {\n marginTop: -4,\n },\n});\n"],"names":["rows","TranscriptGroupView","AssistantTurnView"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import {useEffect}from'react';import {useApolloClient}from'@apollo/client/index.js';import {clearCachedCanvasBoards}from'../canvas/surfaceIndex.js';import {registerMemoryReclaimer}from'./memoryPressure.js';import {useMemoryPressureResponder}from'./useMemoryPressureResponder.js';function MemoryPressureResponder() {
2
+ const apolloClient = useApolloClient();
3
+ useEffect(() => {
4
+ const offApollo = registerMemoryReclaimer({
5
+ id: "apollo-cache",
6
+ reclaim: () => {
7
+ apolloClient.cache.gc();
8
+ }
9
+ });
10
+ const offBoards = registerMemoryReclaimer({
11
+ id: "canvas-boards",
12
+ reclaim: () => clearCachedCanvasBoards()
13
+ });
14
+ return () => {
15
+ offApollo();
16
+ offBoards();
17
+ };
18
+ }, [apolloClient]);
19
+ useMemoryPressureResponder();
20
+ return null;
21
+ }export{MemoryPressureResponder,MemoryPressureResponder as default};//# sourceMappingURL=MemoryPressureResponder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MemoryPressureResponder.js","sources":["../../../src/features/memory/MemoryPressureResponder.tsx"],"sourcesContent":["import React, { useEffect } from 'react';\nimport { useApolloClient } from '@apollo/client';\nimport { clearCachedCanvasBoards } from '../canvas/surfaceIndex';\nimport { registerMemoryReclaimer } from './memoryPressure';\nimport { useMemoryPressureResponder } from './useMemoryPressureResponder';\n\n/**\n * Mount once at the app root. Listens for iOS memory warnings and for\n * backgrounding, and hands memory back instead of waiting to be killed.\n *\n * Renders nothing.\n *\n * Only caches that can be re-derived or re-fetched are dropped here - never\n * user input. The Apollo cache is the big one: `Query.messages` concatenates\n * every page it has ever seen with no cap and no eviction, and nothing in the\n * app had ever called `cache.gc()`.\n */\nexport function MemoryPressureResponder(): null {\n const apolloClient = useApolloClient();\n\n useEffect(() => {\n const offApollo = registerMemoryReclaimer({\n id: 'apollo-cache',\n reclaim: () => {\n // gc() only drops entries nothing references any more, so it\n // cannot invalidate data a mounted screen is still rendering.\n apolloClient.cache.gc();\n },\n });\n const offBoards = registerMemoryReclaimer({\n id: 'canvas-boards',\n reclaim: () => clearCachedCanvasBoards(),\n });\n return () => {\n offApollo();\n offBoards();\n };\n }, [apolloClient]);\n\n useMemoryPressureResponder();\n\n return null;\n}\n\nexport default MemoryPressureResponder;\n"],"names":[],"mappings":"uRAiBO,SAAS,uBAAgC,GAAA;AAC9C,EAAA,MAAM,eAAe,eAAgB,EAAA;AACrC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,YAAY,uBAAwB,CAAA;AAAA,MACxC,EAAI,EAAA,cAAA;AAAA,MACJ,SAAS,MAAM;AAGb,QAAA,YAAA,CAAa,MAAM,EAAG,EAAA;AAAA;AACxB,KACD,CAAA;AACD,IAAA,MAAM,YAAY,uBAAwB,CAAA;AAAA,MACxC,EAAI,EAAA,eAAA;AAAA,MACJ,OAAA,EAAS,MAAM,uBAAwB;AAAA,KACxC,CAAA;AACD,IAAA,OAAO,MAAM;AACX,MAAU,SAAA,EAAA;AACV,MAAU,SAAA,EAAA;AAAA,KACZ;AAAA,GACF,EAAG,CAAC,YAAY,CAAC,CAAA;AACjB,EAA2B,0BAAA,EAAA;AAC3B,EAAO,OAAA,IAAA;AACT"}
@@ -0,0 +1,24 @@
1
+ const reclaimers = /* @__PURE__ */ new Map();
2
+ function registerMemoryReclaimer(reclaimer) {
3
+ reclaimers.set(reclaimer.id, reclaimer);
4
+ return () => {
5
+ if (reclaimers.get(reclaimer.id) === reclaimer) reclaimers.delete(reclaimer.id);
6
+ };
7
+ }
8
+ function runMemoryReclaim(reason) {
9
+ const ran = [];
10
+ const failed = [];
11
+ for (const reclaimer of Array.from(reclaimers.values())) {
12
+ try {
13
+ reclaimer.reclaim(reason);
14
+ ran.push(reclaimer.id);
15
+ } catch (e) {
16
+ failed.push(reclaimer.id);
17
+ }
18
+ }
19
+ return {
20
+ reason,
21
+ ran,
22
+ failed
23
+ };
24
+ }export{registerMemoryReclaimer,runMemoryReclaim};//# sourceMappingURL=memoryPressure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memoryPressure.js","sources":["../../../src/features/memory/memoryPressure.ts"],"sourcesContent":["/**\n * Respond to iOS memory pressure instead of waiting to be killed.\n *\n * iOS sends an app a memory warning BEFORE it evicts it, and it evicts the\n * largest suspended process first. This app never listened for that warning and\n * never released anything - measured 2026-09-18 on a 6GB iPhone 13 Pro, it is\n * terminated within ~5s of being backgrounded, so every return is a cold launch\n * that loses the user's screen and their unsent composer text.\n *\n * Two signals matter and both are handled here:\n * - `memoryWarning`: iOS is asking. Give memory back now.\n * - `background`: the moment our footprint decides whether we survive.\n *\n * Reclaimers are REGISTERED rather than hard-coded so a feature owns its own\n * cache: this module must not have to import the canvas board cache, the Apollo\n * client and the attachment store (which would drag all of them into the launch\n * graph - the very problem `lazyLocalExperience` exists to avoid).\n *\n * This is deliberately conservative: a reclaimer may only drop data that can be\n * re-derived or re-fetched. Nothing here may lose user input.\n */\n\nexport type MemoryReclaimReason = 'memory-warning' | 'background';\n\nexport interface IMemoryReclaimer {\n /** For logs, and so a reclaimer can be replaced rather than duplicated. */\n id: string;\n /**\n * Free what you can. MUST NOT throw - a failing reclaimer must not stop the\n * others, and must never take the app down while it is already under\n * pressure. Return a rough byte estimate if you have one, for logging only.\n */\n reclaim: (reason: MemoryReclaimReason) => void | number;\n}\n\nconst reclaimers = new Map<string, IMemoryReclaimer>();\n\n/** Register (or replace) a reclaimer. Returns an unregister function. */\nexport function registerMemoryReclaimer(reclaimer: IMemoryReclaimer): () => void {\n reclaimers.set(reclaimer.id, reclaimer);\n return () => {\n // Only remove it if it is still the same registration: a later\n // re-register of the same id owns the slot now.\n if (reclaimers.get(reclaimer.id) === reclaimer) reclaimers.delete(reclaimer.id);\n };\n}\n\nexport function registeredMemoryReclaimerIds(): string[] {\n return Array.from(reclaimers.keys());\n}\n\n/** Test seam. */\nexport function __clearMemoryReclaimersForTests(): void {\n reclaimers.clear();\n}\n\nexport interface IMemoryReclaimResult {\n reason: MemoryReclaimReason;\n ran: string[];\n failed: string[];\n}\n\n/**\n * Run every reclaimer. Never throws: one broken reclaimer must not prevent the\n * rest from freeing memory, and must not crash an app that is already being\n * squeezed.\n */\nexport function runMemoryReclaim(reason: MemoryReclaimReason): IMemoryReclaimResult {\n const ran: string[] = [];\n const failed: string[] = [];\n for (const reclaimer of Array.from(reclaimers.values())) {\n try {\n reclaimer.reclaim(reason);\n ran.push(reclaimer.id);\n } catch {\n failed.push(reclaimer.id);\n }\n }\n return { reason, ran, failed };\n}\n"],"names":[],"mappings":"AAiCA,MAAM,UAAA,uBAAiB,GAA8B,EAAA;AAG9C,SAAS,wBAAwB,SAAyC,EAAA;AAC/E,EAAW,UAAA,CAAA,GAAA,CAAI,SAAU,CAAA,EAAA,EAAI,SAAS,CAAA;AACtC,EAAA,OAAO,MAAM;AAGX,IAAI,IAAA,UAAA,CAAW,IAAI,SAAU,CAAA,EAAE,MAAM,SAAW,EAAA,UAAA,CAAW,MAAO,CAAA,SAAA,CAAU,EAAE,CAAA;AAAA,GAChF;AACF;AAoBO,SAAS,iBAAiB,MAAmD,EAAA;AAClF,EAAA,MAAM,MAAgB,EAAC;AACvB,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,KAAA,MAAW,aAAa,KAAM,CAAA,IAAA,CAAK,UAAW,CAAA,MAAA,EAAQ,CAAG,EAAA;AACvD,IAAI,IAAA;AACF,MAAA,SAAA,CAAU,QAAQ,MAAM,CAAA;AACxB,MAAI,GAAA,CAAA,IAAA,CAAK,UAAU,EAAE,CAAA;AAAA,KACf,CAAA,OAAA,CAAA,EAAA;AACN,MAAO,MAAA,CAAA,IAAA,CAAK,UAAU,EAAE,CAAA;AAAA;AAC1B;AAEF,EAAO,OAAA;AAAA,IACL,MAAA;AAAA,IACA,GAAA;AAAA,IACA;AAAA,GACF;AACF"}
@@ -0,0 +1,23 @@
1
+ import {useEffect}from'react';import {AppState}from'react-native';import {runMemoryReclaim}from'./memoryPressure.js';function useMemoryPressureResponder() {
2
+ useEffect(() => {
3
+ const onMemoryWarning = () => {
4
+ const result = runMemoryReclaim("memory-warning");
5
+ if (__DEV__) {
6
+ console.log("[memory] warning -> reclaimed:", result.ran.join(", ") || "(nothing registered)");
7
+ }
8
+ };
9
+ const onAppStateChange = (next) => {
10
+ if (next !== "background") return;
11
+ const result = runMemoryReclaim("background");
12
+ if (__DEV__) {
13
+ console.log("[memory] background -> reclaimed:", result.ran.join(", ") || "(nothing registered)");
14
+ }
15
+ };
16
+ const warningSub = AppState.addEventListener("memoryWarning", onMemoryWarning);
17
+ const stateSub = AppState.addEventListener("change", onAppStateChange);
18
+ return () => {
19
+ warningSub.remove();
20
+ stateSub.remove();
21
+ };
22
+ }, []);
23
+ }export{useMemoryPressureResponder as default,useMemoryPressureResponder};//# sourceMappingURL=useMemoryPressureResponder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMemoryPressureResponder.js","sources":["../../../src/features/memory/useMemoryPressureResponder.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { AppState, AppStateStatus } from 'react-native';\nimport { runMemoryReclaim } from './memoryPressure';\n\n/**\n * Wire the reclaimers to the two signals that matter. Mount ONCE, at the app\n * root.\n *\n * `memoryWarning` is a real React Native AppState event (see\n * react-native/Libraries/AppState/AppState.js) that nothing in this app was\n * listening to, even though it is iOS telling us we are about to be killed.\n */\nexport function useMemoryPressureResponder(): void {\n useEffect(() => {\n const onMemoryWarning = () => {\n const result = runMemoryReclaim('memory-warning');\n if (__DEV__) {\n // eslint-disable-next-line no-console\n console.log('[memory] warning -> reclaimed:', result.ran.join(', ') || '(nothing registered)');\n }\n };\n\n const onAppStateChange = (next: AppStateStatus) => {\n // Only 'background'. 'inactive' fires for the app switcher peek and\n // for call banners, and throwing caches away on those would make the\n // common case (a glance, then straight back) slower for no gain.\n if (next !== 'background') return;\n const result = runMemoryReclaim('background');\n if (__DEV__) {\n // eslint-disable-next-line no-console\n console.log('[memory] background -> reclaimed:', result.ran.join(', ') || '(nothing registered)');\n }\n };\n\n const warningSub = AppState.addEventListener('memoryWarning', onMemoryWarning);\n const stateSub = AppState.addEventListener('change', onAppStateChange);\n return () => {\n warningSub.remove();\n stateSub.remove();\n };\n }, []);\n}\n\nexport default useMemoryPressureResponder;\n"],"names":[],"mappings":"qHAYO,SAAS,0BAAmC,GAAA;AACjD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,kBAAkB,MAAM;AAC5B,MAAM,MAAA,MAAA,GAAS,iBAAiB,gBAAgB,CAAA;AAChD,MAAA,IAAI,OAAS,EAAA;AAEX,QAAA,OAAA,CAAQ,IAAI,gCAAkC,EAAA,MAAA,CAAO,IAAI,IAAK,CAAA,IAAI,KAAK,sBAAsB,CAAA;AAAA;AAC/F,KACF;AACA,IAAM,MAAA,gBAAA,GAAmB,CAAC,IAAyB,KAAA;AAIjD,MAAA,IAAI,SAAS,YAAc,EAAA;AAC3B,MAAM,MAAA,MAAA,GAAS,iBAAiB,YAAY,CAAA;AAC5C,MAAA,IAAI,OAAS,EAAA;AAEX,QAAA,OAAA,CAAQ,IAAI,mCAAqC,EAAA,MAAA,CAAO,IAAI,IAAK,CAAA,IAAI,KAAK,sBAAsB,CAAA;AAAA;AAClG,KACF;AACA,IAAA,MAAM,UAAa,GAAA,QAAA,CAAS,gBAAiB,CAAA,eAAA,EAAiB,eAAe,CAAA;AAC7E,IAAA,MAAM,QAAW,GAAA,QAAA,CAAS,gBAAiB,CAAA,QAAA,EAAU,gBAAgB,CAAA;AACrE,IAAA,OAAO,MAAM;AACX,MAAA,UAAA,CAAW,MAAO,EAAA;AAClB,MAAA,QAAA,CAAS,MAAO,EAAA;AAAA,KAClB;AAAA,GACF,EAAG,EAAE,CAAA;AACP"}
@@ -191,6 +191,13 @@ async function mintSurfaceSystemToken(input) {
191
191
  }
192
192
  try {
193
193
  const obtained = await gqlFetch(graphqlUrl, idToken, OBTAIN_USER_SYSTEM_TOKEN, {
194
+ // No `algorithm`: the backend signs with its platform default
195
+ // (DEFAULT_REGISTRY_API_TOKEN_ALGORITHM - RS256 on production).
196
+ // Pinning HS256 here flipped the user's ONE system-token row to
197
+ // HS256 on prod, and every other client (the web shell, the
198
+ // hosted cdecli-agent, the issues/calendar lanes) then revealed
199
+ // and presented that HS256 JWT: "invalid HS256 signature" at
200
+ // the agent, UNAUTHENTICATED at the subgraphs.
194
201
  input: {
195
202
  projectId,
196
203
  environmentTagId,
@@ -233,6 +240,7 @@ async function mintSurfaceSystemToken(input) {
233
240
  let tokenId = (_q = (_p = (_o = getResult.data) == null ? void 0 : _o.getUserSystemToken) == null ? void 0 : _p.id) != null ? _q : null;
234
241
  if (!tokenId) {
235
242
  const createResult = await gqlFetch(graphqlUrl, idToken, CREATE_SECRET_API_TOKEN, {
243
+ // Platform-default algorithm, same reason as the obtain call above.
236
244
  input: {
237
245
  name: "System Token",
238
246
  expiryInSeconds: 31536e3,