@jetcrab/pi-desk 1.1.16 → 1.1.20-dev

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 (106) hide show
  1. package/docs/plugins.md +3 -1
  2. package/next.config.ts +1 -0
  3. package/package.json +4 -3
  4. package/src/client/l2_biz/workbench/file-workspace/l2-file-workspace-runtime.ts +1 -1
  5. package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-reload.ts +42 -0
  6. package/src/client/l2_biz/workbench/l2-work-session-sidebar.tsx +19 -1
  7. package/src/client/l2_biz/workbench/l2-workbench-chat.ts +45 -0
  8. package/src/client/l2_biz/workbench/l2-workbench.tsx +4 -0
  9. package/src/client/l3_modules/conversation/l3-conversation-markdown.module.css +1 -0
  10. package/src/client/l3_modules/conversation/l3-conversation-markdown.tsx +10 -1
  11. package/src/client/l3_modules/project-files/l3-markdown-preview.tsx +10 -1
  12. package/src/client/l3_modules/project-files/l3-project-document-view.tsx +2 -0
  13. package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +171 -229
  14. package/src/client/l3_modules/project-files/l3-project-file-runtime.ts +133 -70
  15. package/src/client/l3_modules/project-files/l3-project-reader.ts +4 -3
  16. package/src/client/l3_modules/project-files/views/l3-project-search-controls.tsx +201 -0
  17. package/src/client/l3_modules/project-files/views/l3-project-search-results.tsx +333 -0
  18. package/src/client/l4_foundation/ui/code/l4-code-preview.tsx +47 -3
  19. package/src/client/l4_foundation/ui/markdown/l4-markdown-remark.ts +89 -0
  20. package/src/client/l4_foundation/ui/markdown/l4-markdown.module.css +58 -0
  21. package/src/client/l4_foundation/ui/markdown/l4-markdown.tsx +108 -0
  22. package/src/common/l2_biz/work-session/l2-work-session-file-contract.ts +4 -3
  23. package/src/common/l2_biz/workbench/l2-workbench-locale-messages.ts +6 -0
  24. package/src/common/l3_modules/project-files/l3-project-files-contract.ts +2 -3
  25. package/src/common/l3_modules/project-files/l3-project-files-locale-messages.ts +56 -10
  26. package/src/common/l4_foundation/file/l4-project-file-contract.ts +51 -1
  27. package/src/common/l4_foundation/locale/l4-host-error-messages.ts +11 -0
  28. package/src/common/l4_foundation/locale/l4-locale-messages.ts +16 -0
  29. package/src/instrumentation.ts +0 -2
  30. package/src/server/l1_entry/api/l1-project-read-routes.ts +6 -4
  31. package/src/server/l1_entry/api/l1-project-route.ts +9 -8
  32. package/src/server/l1_entry/api/l1-work-session-file-error.ts +23 -0
  33. package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +0 -3
  34. package/src/server/l2_biz/work-session/l2-work-session-chat-runtime.ts +30 -58
  35. package/src/server/l2_biz/work-session/l2-work-session-manage.ts +0 -4
  36. package/src/server/l4_foundation/file/l4-project-file-index.ts +276 -0
  37. package/src/server/l4_foundation/file/l4-project-search.ts +399 -0
  38. package/src/server/l4_foundation/file/l4-ripgrep-search.ts +192 -0
  39. package/src/server/l4_foundation/file/l4-work-session-project-file.ts +8 -332
  40. package/src/server/l4_foundation/pi/l4-pi-package-maintenance.ts +0 -6
  41. package/src/server/l4_foundation/pi/l4-pi-plugin-capability-probe.ts +1 -3
  42. package/src/server/l4_foundation/pi/l4-pi-plugin-probe.ts +1 -3
  43. package/src/server/l4_foundation/pi/l4-pi-runner-process.ts +3 -15
  44. package/src/server/l4_foundation/process/l4-process-tree.js +31 -5
  45. package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
  46. package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
  47. package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
  48. package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -1
  49. package/temp/build/pi-desk/release/.next/required-server-files.js +4 -3
  50. package/temp/build/pi-desk/release/.next/required-server-files.json +4 -3
  51. package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  52. package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
  53. package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +1 -1
  54. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  57. package/temp/build/pi-desk/release/.next/server/app/_not-found/page.js +1 -1
  58. package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  59. package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
  60. package/temp/build/pi-desk/release/.next/server/app/api/project-files/get/route.js +2 -1
  61. package/temp/build/pi-desk/release/.next/server/app/api/project-files/list/route.js +2 -1
  62. package/temp/build/pi-desk/release/.next/server/app/api/project-files/reveal/route.js +1 -1
  63. package/temp/build/pi-desk/release/.next/server/app/api/project-files/search/route.js +2 -1
  64. package/temp/build/pi-desk/release/.next/server/app/api/project-git/get/route.js +2 -1
  65. package/temp/build/pi-desk/release/.next/server/app/api/project-git-branches/list/route.js +2 -1
  66. package/temp/build/pi-desk/release/.next/server/app/api/project-git-changes/list/route.js +2 -1
  67. package/temp/build/pi-desk/release/.next/server/app/api/project-git-diff/get/route.js +2 -1
  68. package/temp/build/pi-desk/release/.next/server/app/api/project-git-heads/get/route.js +2 -1
  69. package/temp/build/pi-desk/release/.next/server/app/api/project-git-log/list/route.js +2 -1
  70. package/temp/build/pi-desk/release/.next/server/app/login/page.js +1 -1
  71. package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
  72. package/temp/build/pi-desk/release/.next/server/app/page.js +77 -77
  73. package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
  74. package/temp/build/pi-desk/release/.next/server/chunks/{33658.js → 10881.js} +8 -8
  75. package/temp/build/pi-desk/release/.next/server/chunks/23054.js +1 -1
  76. package/temp/build/pi-desk/release/.next/server/chunks/25406.js +3 -3
  77. package/temp/build/pi-desk/release/.next/server/chunks/35985.js +5 -5
  78. package/temp/build/pi-desk/release/.next/server/chunks/50520.js +2 -4
  79. package/temp/build/pi-desk/release/.next/server/chunks/6917.js +1 -1
  80. package/temp/build/pi-desk/release/.next/server/chunks/70780.js +1 -0
  81. package/temp/build/pi-desk/release/.next/server/chunks/74528.js +1 -1
  82. package/temp/build/pi-desk/release/.next/server/chunks/92386.js +1 -1
  83. package/temp/build/pi-desk/release/.next/server/instrumentation.js +1 -1
  84. package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
  85. package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
  86. package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
  87. package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
  88. package/temp/build/pi-desk/release/.next/static/chunks/27611-0890482fe76bd9d6.js +238 -0
  89. package/temp/build/pi-desk/release/.next/static/chunks/{31087-ba426d09d6f68bfd.js → 31087-d8e5765d541d5c26.js} +1 -1
  90. package/temp/build/pi-desk/release/.next/static/chunks/{49516-cc4dcd84d64ab4fe.js → 51474-8dc714cb76b2e60a.js} +2 -2
  91. package/temp/build/pi-desk/release/.next/static/chunks/77592-5bef6acb8e7c4b96.js +13 -0
  92. package/temp/build/pi-desk/release/.next/static/chunks/app/layout-a50c456648537afb.js +3 -0
  93. package/temp/build/pi-desk/release/.next/static/chunks/app/{page-0069319a4fb97211.js → page-0789c43551b29dd2.js} +4 -4
  94. package/temp/build/pi-desk/release/.next/static/chunks/{webpack-40a782fc8a3535b9.js → webpack-d7248206b7fc7ba8.js} +1 -1
  95. package/temp/build/pi-desk/release/.next/static/css/250a6b93cc2222d1.css +1 -0
  96. package/temp/build/pi-desk/release/.next/static/css/6dbae2922f88ca31.css +1 -0
  97. package/temp/build/pi-desk/release/.next/static/css/a4cf28fd76c70165.css +1 -0
  98. package/temp/build/pi-desk/release/.next/trace-build +1 -1
  99. package/temp/build/pi-desk/release/.next/server/chunks/8584.js +0 -2
  100. package/temp/build/pi-desk/release/.next/static/chunks/39654-b5fedd762ea0312e.js +0 -238
  101. package/temp/build/pi-desk/release/.next/static/chunks/75130-d2ca74894aff8085.js +0 -13
  102. package/temp/build/pi-desk/release/.next/static/chunks/app/layout-c0e1c5cef8620cf8.js +0 -3
  103. package/temp/build/pi-desk/release/.next/static/css/6427d02b6edf406f.css +0 -1
  104. package/temp/build/pi-desk/release/.next/static/css/949d0d1a99d5de92.css +0 -1
  105. /package/temp/build/pi-desk/release/.next/static/{DqamF28k0ShQLbNVzXDIj → RLKcK-RQF5FvEeDmS9uih}/_buildManifest.js +0 -0
  106. /package/temp/build/pi-desk/release/.next/static/{DqamF28k0ShQLbNVzXDIj → RLKcK-RQF5FvEeDmS9uih}/_ssgManifest.js +0 -0
package/docs/plugins.md CHANGED
@@ -99,6 +99,8 @@ react-dom/client
99
99
  @jetcrab/pi-desk-sdk/react/markdown
100
100
  ```
101
101
 
102
+ `PluginMarkdown` 会高亮带语言标记的代码块(例如 `sql`),语言和主题资源按需加载;插件不需要另装高亮库。它只负责展示,不格式化代码;SQL 等业务格式化依赖由对应插件维护。
103
+
102
104
  复用宿主 React 与 UI,不复制或打包第二套公共运行时。`/browser` 不在白名单中:普通函数无需包装;`copyBrowserText` 等 SDK Browser 运行函数需要构建时打入插件模块,不能原样留下裸 import。其他业务依赖也需构建为相对模块。无构建示例用 JS `createElement`;TSX、TypeScript 浏览器文件需要先编译为 JS。
103
105
 
104
106
  Application 使用 `chrome: 'host'` 让宿主负责 Dialog、标题、关闭、焦点、Escape、Loading/Error 和视口约束;内容自然决定尺寸,不替换工作台。React mount 使用 `createRoot`、`PluginHostProvider`、`PluginErrorBoundary` 和公共控件;disposer 调用 `root.unmount()`,释放自建 Listener、Timer、Observer、Object URL。宿主先 abort `signal`,迟到结果必须忽略。
@@ -209,7 +211,7 @@ export default {
209
211
  - 首屏和实时必需内容放 `summary`;大内容或用户展开后才用的数据放 `detail`。需要详情时,仅在消息已有 `entryId` 且 `fixed.hasDetail` 为真后调用 `target.message.loadDetail()`;异步结果须检查挂载的 `signal`,不能写回失效视图。
210
212
  - 视图从 `getSnapshot()` 读取完整当前状态,并通过 `subscribe()` 跟随更新;同 `viewKey` 的消息更新不重新挂载,Temporary 转 Durable 保持同一 Handle,`viewKey` 改变时会重挂。释放时解除订阅并清理自有资源。
211
213
  - Declaration 和 View 分别按 `priority` 选择,数值越大越优先;同最高优先级冲突会报错。声明、加载或展示失败不自动尝试低优先级实现;异步展示错误通过 `target.reportError(error)` 交给宿主处理。
212
- - 刷新后仍需展示的数据必须存在于最终 Pi Entry,仅通过 Tool `onUpdate()` 提供的数据不会自动持久化。更新插件时兼容已有消息结构,不靠自动重载旧会话解决兼容问题。
214
+ - 刷新后仍需展示的数据必须存在于最终 Pi Entry,仅通过 Tool `onUpdate()` 提供的数据不会自动持久化。插件初始化、更新和热加载不自动重投影或重传旧聊天,新消息使用当前声明;Browser View 需兼容已有消息结构,不靠自动重载旧会话解决兼容问题。
213
215
 
214
216
  ## 任务
215
217
 
package/next.config.ts CHANGED
@@ -42,6 +42,7 @@ const nextConfig: NextConfig = {
42
42
  ? 'temp/next-dev'
43
43
  : 'temp/build/pi-desk/release/.next',
44
44
  serverExternalPackages: [
45
+ '@vscode/ripgrep',
45
46
  '@earendil-works/pi-coding-agent',
46
47
  '@earendil-works/pi-agent-core',
47
48
  '@earendil-works/pi-ai',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetcrab/pi-desk",
3
- "version": "1.1.16",
3
+ "version": "1.1.20-dev",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,6 +37,7 @@
37
37
  "@streamdown/code": "^1.1.1",
38
38
  "@streamdown/math": "^1.0.2",
39
39
  "@streamdown/mermaid": "^1.0.2",
40
+ "@vscode/ripgrep": "1.18.0",
40
41
  "@xterm/addon-fit": "0.11.0",
41
42
  "@xterm/addon-serialize": "0.14.0",
42
43
  "@xterm/headless": "6.0.0",
@@ -69,7 +70,7 @@
69
70
  "viewerjs": "^1.11.8",
70
71
  "ws": "^8.21.3",
71
72
  "zod": "^4.4.3",
72
- "@jetcrab/pi-desk-sdk": "^1.1.6"
73
+ "@jetcrab/pi-desk-sdk": "^1.1.7-dev"
73
74
  },
74
75
  "devDependencies": {
75
76
  "@earendil-works/pi-agent-core": "1.2.0",
@@ -111,7 +112,7 @@
111
112
  "build:plugin-host-runtime": "pnpm --filter @jetcrab/pi-desk-sdk build",
112
113
  "start": "node bin/pi-desk.js --no-open",
113
114
  "test:package": "node --test tests/l4-pi-desk-package.test.mjs",
114
- "test:pidesk": "node --import tsx --test tests/l4-pidesk-command.test.ts tests/l2-plugin-commands.test.ts tests/l4-pidesk-session-reload.test.ts tests/l4-pidesk-runtime.test.ts tests/l4-pidesk-runtime-bundle.test.mjs tests/l4-pidesk-docs.test.mjs tests/l4-pidesk-installed-runners.test.mjs tests/l4-pidesk-package-maintenance.test.ts tests/l4-pidesk-release-gate.test.mjs tests/l4-pi-global-runtime.test.mjs tests/l4-pi-desk-supervisor-cli.test.mjs",
115
+ "test:pidesk": "node --import tsx --test tests/l4-pidesk-command.test.ts tests/l2-plugin-commands.test.ts tests/l4-pidesk-session-reload.test.ts tests/l4-pidesk-runtime.test.ts tests/l4-pidesk-runtime-bundle.test.mjs tests/l4-pidesk-docs.test.mjs tests/l4-pidesk-installed-runners.test.mjs tests/l4-pidesk-package-maintenance.test.ts tests/l4-pidesk-release-gate.test.mjs tests/l4-pi-desk-process-tree.test.mjs tests/l4-pi-runner-process.test.ts tests/l4-pi-global-runtime.test.mjs tests/l4-pi-desk-supervisor-cli.test.mjs",
115
116
  "test:app-websocket": "pnpm build && node --import tsx --test tests/l2-chat-state.test.ts tests/l2-workbench-chat-repository.test.ts tests/l2-workbench-chat.test.ts tests/l2-workbench-layout.test.ts tests/l2-work-session-column-layout.test.ts tests/l2-work-session-update.test.ts tests/l1-app-websocket.test.mjs",
116
117
  "lint": "eslint .",
117
118
  "lint:production": "eslint . --ignore-pattern \"apps/desktop/**\" --ignore-pattern \"**/tests/**\" --ignore-pattern \"**/__tests__/**\" --ignore-pattern \"**/*.test.*\" --ignore-pattern \"**/*.spec.*\"",
@@ -346,7 +346,7 @@ export class L2FileWorkspaceRuntime {
346
346
  cwd,
347
347
  {
348
348
  list: (path, signal) => this.biz.list({ ...context, path }, signal),
349
- search: (query, signal) => this.biz.search({ ...context, query }, signal),
349
+ search: (input, signal) => this.biz.search({ ...context, ...input }, signal),
350
350
  get: (path, imagePreviewMode, signal) =>
351
351
  this.biz.get({ ...context, path, imagePreviewMode }, signal),
352
352
  diff: (repositoryRoot, path, signal) =>
@@ -0,0 +1,42 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useMemo, useSyncExternalStore } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import type { L2WorkSessionListItem } from '@common/l2_biz/work-session/l2-work-session-contract'
6
+ import { useL4AppToast } from '@client/l4_foundation/ui/l4-app-toast'
7
+ import type { L2WorkbenchChatRuntime } from '../l2-workbench-chat'
8
+
9
+ export function useL2WorkbenchChatReload(
10
+ runtime: L2WorkbenchChatRuntime,
11
+ workSessions: readonly L2WorkSessionListItem[]
12
+ ): {
13
+ syncingWorkIds: readonly string[]
14
+ reload: (workSession: L2WorkSessionListItem) => void
15
+ } {
16
+ const { t } = useTranslation('workbench')
17
+ const toast = useL4AppToast()
18
+ const getSyncing = useCallback(
19
+ (): string =>
20
+ JSON.stringify(
21
+ workSessions
22
+ .filter(
23
+ ({ workId, sessionId, branchId }) =>
24
+ runtime.getSourceState({ workId, sessionId, branchId }).syncStatus === 'syncing'
25
+ )
26
+ .map((session) => session.workId)
27
+ ),
28
+ [runtime, workSessions]
29
+ )
30
+ const syncing = useSyncExternalStore(runtime.subscribe, getSyncing, getSyncing)
31
+ const syncingWorkIds = useMemo((): string[] => JSON.parse(syncing), [syncing])
32
+ const reload = useCallback(
33
+ (session: L2WorkSessionListItem): void => {
34
+ const { workId, sessionId, branchId } = session
35
+ void runtime.reloadMessages({ workId, sessionId, branchId }).catch((cause: unknown) => {
36
+ toast.error(cause instanceof Error ? cause.message : t('reloadChatDataFailed'))
37
+ })
38
+ },
39
+ [runtime, t, toast]
40
+ )
41
+ return { syncingWorkIds, reload }
42
+ }
@@ -140,6 +140,8 @@ interface L2WorkSessionSidebarProps {
140
140
  onTogglePin: (workId: string) => void
141
141
  onDelete: (workId: string) => void
142
142
  onRename: (workSession: L2WorkSessionListItem) => void
143
+ syncingWorkIds: readonly string[]
144
+ onReloadChat: (workSession: L2WorkSessionListItem) => void
143
145
  onArrangementChange: (arrangement: L2WorkSessionArrangement, movedWorkId: string) => void
144
146
  }
145
147
 
@@ -343,7 +345,9 @@ const SortableWorkSessionRow = memo(function SortableWorkSessionRow({
343
345
  onSelect,
344
346
  onTogglePin,
345
347
  onDelete,
346
- onRename
348
+ onRename,
349
+ syncing,
350
+ onReloadChat
347
351
  }: {
348
352
  workSession: L2WorkSessionListItem
349
353
  mobile: boolean
@@ -357,6 +361,8 @@ const SortableWorkSessionRow = memo(function SortableWorkSessionRow({
357
361
  onTogglePin: (workId: string) => void
358
362
  onDelete: (workId: string) => void
359
363
  onRename: (workSession: L2WorkSessionListItem) => void
364
+ syncing: boolean
365
+ onReloadChat: (workSession: L2WorkSessionListItem) => void
360
366
  }): React.JSX.Element {
361
367
  const suppressTouchClick = useRef(false)
362
368
  const lastPointer = useRef('')
@@ -606,6 +612,14 @@ const SortableWorkSessionRow = memo(function SortableWorkSessionRow({
606
612
  <PencilIcon />
607
613
  {t('renameSessionTitle')}
608
614
  </ContextMenuItem>
615
+ <ContextMenuItem
616
+ data-testid={`work-session-reload-chat-${workSession.workId}`}
617
+ disabled={actionsDisabled || deleting || syncing}
618
+ onClick={() => onReloadChat(workSession)}
619
+ >
620
+ {syncing ? <LoaderCircleIcon className="animate-spin" /> : <RefreshCwIcon />}
621
+ {t('reloadChatData')}
622
+ </ContextMenuItem>
609
623
  <ContextMenuItem
610
624
  data-testid={`work-session-copy-project-path-${workSession.workId}`}
611
625
  onClick={copyProjectPath}
@@ -675,6 +689,8 @@ function L2WorkSessionSidebarComponent({
675
689
  onTogglePin,
676
690
  onDelete,
677
691
  onRename,
692
+ syncingWorkIds,
693
+ onReloadChat,
678
694
  onArrangementChange
679
695
  }: L2WorkSessionSidebarProps): React.JSX.Element {
680
696
  const { t } = useTranslation('workbench')
@@ -888,6 +904,8 @@ function L2WorkSessionSidebarComponent({
888
904
  onTogglePin={onTogglePin}
889
905
  onDelete={onDelete}
890
906
  onRename={onRename}
907
+ syncing={syncingWorkIds.includes(workSession.workId)}
908
+ onReloadChat={onReloadChat}
891
909
  />
892
910
  )
893
911
  })
@@ -73,6 +73,7 @@ export interface L2WorkbenchChatRuntime {
73
73
  ) => Promise<L2ChatMessageDetail>
74
74
  compact: (source: L2ChatSource) => Promise<void>
75
75
  reload: (source: L2ChatSource, mode?: L2ChatMode) => Promise<void>
76
+ reloadMessages: (source: L2ChatSource) => Promise<void>
76
77
  setPresentation: (source: L2ChatSource, mode: L2ChatMode) => Promise<void>
77
78
  disconnected: () => void
78
79
  dispose: () => void
@@ -611,6 +612,50 @@ export function createL2WorkbenchChatRuntime(
611
612
  )
612
613
  },
613
614
 
615
+ async reloadMessages(sourceInput): Promise<void> {
616
+ const source = L2ChatSourceSchema.parse(sourceInput)
617
+ const key = sourceKey(source)
618
+ const workSession = currentWorkSessions.get(source.workId)
619
+ if (!workSession || !sameSource(sourceFor(workSession), source)) {
620
+ throw new Error(l2WorkbenchText('reloadChatSourceChanged'))
621
+ }
622
+ const previous = statesBySource.get(key)
623
+ if (
624
+ !workSessionsReady ||
625
+ disposed ||
626
+ pendingSyncs.has(key) ||
627
+ previous?.syncStatus === 'syncing'
628
+ ) {
629
+ throw new Error(l2WorkbenchText('sourceSyncing'))
630
+ }
631
+ const epoch = connectionEpoch
632
+ const queue = eventQueues.get(key)
633
+ const previousSyncEpoch = queue?.syncEpoch ?? null
634
+ if (queue) queue.syncEpoch = null
635
+ subscriptionPool.set(source.workId, source)
636
+ setSourceState(source, { ...(previous ?? EMPTY_CHAT_SOURCE_STATE), syncStatus: 'syncing' })
637
+ try {
638
+ // 重拉期间继续接收旧基线上的实时消息,失败时无需丢弃缓存或补偿重放。
639
+ await appSocket.request(L2ChatSocketContracts.subscribe, {
640
+ subscriptions: [{ source, cursor: null }]
641
+ })
642
+ } catch (error) {
643
+ if (!disposed && epoch === connectionEpoch && isCurrentAndSubscribed(source)) {
644
+ if (queue && eventQueues.get(key) === queue) queue.syncEpoch = previousSyncEpoch
645
+ const current = statesBySource.get(key)
646
+ if (current)
647
+ setSourceState(source, { ...current, syncStatus: previous?.syncStatus ?? 'loading' })
648
+ }
649
+ console.error('[Pi Desk][WorkbenchChat] 重新加载聊天数据失败,保留已有缓存', {
650
+ workId: source.workId,
651
+ sessionId: source.sessionId,
652
+ branchId: source.branchId,
653
+ message: error instanceof Error ? error.message : String(error)
654
+ })
655
+ throw error
656
+ }
657
+ },
658
+
614
659
  async setPresentation(sourceInput, mode): Promise<void> {
615
660
  const source = L2ChatSourceSchema.parse(sourceInput)
616
661
  const key = sourceKey(source)
@@ -112,6 +112,7 @@ import {
112
112
  } from './l2-work-session-sidebar-order'
113
113
  import { useL2WorkbenchBranchPicker } from './hooks/l2-workbench-branch-picker'
114
114
  import { useL2WorkSessionRename } from './hooks/l2-work-session-rename'
115
+ import { useL2WorkbenchChatReload } from './hooks/l2-workbench-chat-reload'
115
116
  import { L2WorkSessionRenameDialog } from './views/l2-work-session-rename-dialog'
116
117
  import { useL2WorkbenchPluginHealth } from './hooks/l2-workbench-plugin-health'
117
118
  import { useL2WorkbenchSessionHistory } from './hooks/l2-workbench-session-history'
@@ -370,6 +371,7 @@ export function L2Workbench({
370
371
  createL2WorkbenchChatRuntime(appSocket, (input) => workbenchBiz.getChatMessageDetail(input)),
371
372
  [appSocket, workbenchBiz]
372
373
  )
374
+ const chatReload = useL2WorkbenchChatReload(chatRuntime, workSessions)
373
375
  const chatInputRuntime = useMemo(
374
376
  () => createL2WorkbenchChatInputRuntime(workbenchBiz, chatRuntime),
375
377
  [chatRuntime, workbenchBiz]
@@ -1928,6 +1930,8 @@ export function L2Workbench({
1928
1930
  onTogglePin={togglePinnedWorkSession}
1929
1931
  onDelete={requestDeleteWorkSession}
1930
1932
  onRename={renameSession.show}
1933
+ syncingWorkIds={chatReload.syncingWorkIds}
1934
+ onReloadChat={chatReload.reload}
1931
1935
  onArrangementChange={requestArrangementUpdate}
1932
1936
  />
1933
1937
  )
@@ -1,6 +1,7 @@
1
1
  .markdown {
2
2
  font-size: var(--chat-body-font-size, 0.875rem);
3
3
  line-height: 1.7;
4
+ overflow-wrap: anywhere;
4
5
  }
5
6
 
6
7
  .markdown > * {
@@ -10,6 +10,12 @@ import { MessageResponse } from '@client/l4_foundation/ui/ai-elements/message'
10
10
  import { copyL4BrowserText } from '@client/l4_foundation/lib/l4-browser-clipboard'
11
11
  import { cn } from '@client/l4_foundation/lib/l4-utils'
12
12
  import { L4ImageViewer } from '@client/l4_foundation/ui/l4-image-viewer'
13
+ import {
14
+ L4_MARKDOWN_ALLOWED_TAGS,
15
+ L4_MARKDOWN_CLASS_NAME,
16
+ L4_MARKDOWN_COMPONENTS,
17
+ L4_MARKDOWN_REMARK_PLUGINS
18
+ } from '@client/l4_foundation/ui/markdown/l4-markdown'
13
19
  import { useL4AppToast } from '@client/l4_foundation/ui/l4-app-toast'
14
20
  import styles from './l3-conversation-markdown.module.css'
15
21
 
@@ -140,11 +146,14 @@ export function L3ConversationMarkdown({
140
146
  return (
141
147
  <div className="contents" onClickCapture={handleCodeCopyFallback}>
142
148
  <MessageResponse
143
- className={cn('space-y-0', styles.markdown, className)}
149
+ className={cn('space-y-0', styles.markdown, L4_MARKDOWN_CLASS_NAME, className)}
144
150
  controls={COMPACT_CODE_CONTROLS}
145
151
  isAnimating={isAnimating}
146
152
  lineNumbers={false}
153
+ allowedTags={L4_MARKDOWN_ALLOWED_TAGS}
154
+ remarkPlugins={L4_MARKDOWN_REMARK_PLUGINS}
147
155
  components={{
156
+ ...L4_MARKDOWN_COMPONENTS,
148
157
  ...components,
149
158
  a: L3ConversationMarkdownLink,
150
159
  img: L3ConversationMarkdownImage
@@ -4,6 +4,12 @@ import { cjk } from '@streamdown/cjk'
4
4
  import { code } from '@streamdown/code'
5
5
  import { memo, useLayoutEffect, useRef } from 'react'
6
6
  import { Streamdown } from 'streamdown'
7
+ import {
8
+ L4_MARKDOWN_ALLOWED_TAGS,
9
+ L4_MARKDOWN_CLASS_NAME,
10
+ L4_MARKDOWN_COMPONENTS,
11
+ L4_MARKDOWN_REMARK_PLUGINS
12
+ } from '@client/l4_foundation/ui/markdown/l4-markdown'
7
13
  import styles from './l3-markdown-preview.module.css'
8
14
 
9
15
  const plugins = { cjk, code }
@@ -37,7 +43,10 @@ export const L3MarkdownPreview = memo(function L3MarkdownPreview({
37
43
  onScroll={(event) => onScroll(event.currentTarget.scrollTop)}
38
44
  >
39
45
  <Streamdown
40
- className={styles.document}
46
+ className={`${styles.document} ${L4_MARKDOWN_CLASS_NAME}`}
47
+ allowedTags={L4_MARKDOWN_ALLOWED_TAGS}
48
+ components={L4_MARKDOWN_COMPONENTS}
49
+ remarkPlugins={L4_MARKDOWN_REMARK_PLUGINS}
41
50
  mode="static"
42
51
  parseIncompleteMarkdown={false}
43
52
  isAnimating={false}
@@ -418,6 +418,8 @@ export function L3ProjectDocumentView({
418
418
  }}
419
419
  onOpenDiff={openDiff}
420
420
  savedState={document.editorState}
421
+ navigation={runtime.pendingNavigation}
422
+ onNavigation={(target, exists) => runtime.consumeNavigation(target, exists)}
421
423
  onSaveState={(path, state) => runtime.saveEditorState(path, state)}
422
424
  onEscape={escape}
423
425
  />