cli-jaw 2.2.3 → 2.2.4-preview.20260703203439

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 (216) hide show
  1. package/README.ja.md +1 -4
  2. package/README.ko.md +1 -4
  3. package/README.md +4 -7
  4. package/README.zh-CN.md +1 -4
  5. package/dist/bin/commands/bgtask.js +3 -1
  6. package/dist/bin/commands/bgtask.js.map +1 -1
  7. package/dist/bin/commands/chat.js +2 -2
  8. package/dist/bin/commands/chat.js.map +1 -1
  9. package/dist/bin/commands/dispatch.js +10 -3
  10. package/dist/bin/commands/dispatch.js.map +1 -1
  11. package/dist/bin/commands/tui/fullscreen-mode.js +44 -11
  12. package/dist/bin/commands/tui/fullscreen-mode.js.map +1 -1
  13. package/dist/bin/commands/tui/ws-handler.js +15 -1
  14. package/dist/bin/commands/tui/ws-handler.js.map +1 -1
  15. package/dist/scripts/fresh-install-smoke.js +1 -1
  16. package/dist/scripts/fresh-install-smoke.js.map +1 -1
  17. package/dist/server.js +1 -1
  18. package/dist/server.js.map +1 -1
  19. package/dist/src/agent/agy-bootstrap.js +104 -17
  20. package/dist/src/agent/agy-bootstrap.js.map +1 -1
  21. package/dist/src/agent/agy-runtime.js +104 -0
  22. package/dist/src/agent/agy-runtime.js.map +1 -1
  23. package/dist/src/agent/agy-transcript-watcher.js +14 -0
  24. package/dist/src/agent/agy-transcript-watcher.js.map +1 -1
  25. package/dist/src/agent/args.js +16 -83
  26. package/dist/src/agent/args.js.map +1 -1
  27. package/dist/src/agent/error-classifier.js +1 -3
  28. package/dist/src/agent/error-classifier.js.map +1 -1
  29. package/dist/src/agent/events/claude.js +60 -3
  30. package/dist/src/agent/events/claude.js.map +1 -1
  31. package/dist/src/agent/events/helpers.js +1 -1
  32. package/dist/src/agent/events/helpers.js.map +1 -1
  33. package/dist/src/agent/events/index.js +27 -27
  34. package/dist/src/agent/events/index.js.map +1 -1
  35. package/dist/src/agent/events/summary.js +0 -21
  36. package/dist/src/agent/events/summary.js.map +1 -1
  37. package/dist/src/agent/events/tool-labels.js +0 -28
  38. package/dist/src/agent/events/tool-labels.js.map +1 -1
  39. package/dist/src/agent/jwc-event-mapper.js +3 -3
  40. package/dist/src/agent/jwc-event-mapper.js.map +1 -1
  41. package/dist/src/agent/jwc-runtime.js +5 -1
  42. package/dist/src/agent/jwc-runtime.js.map +1 -1
  43. package/dist/src/agent/kiro-runtime.js +40 -14
  44. package/dist/src/agent/kiro-runtime.js.map +1 -1
  45. package/dist/src/agent/lifecycle-handler.js +2 -77
  46. package/dist/src/agent/lifecycle-handler.js.map +1 -1
  47. package/dist/src/agent/prompt-context.js +38 -0
  48. package/dist/src/agent/prompt-context.js.map +1 -0
  49. package/dist/src/agent/resume-classifier.js +0 -4
  50. package/dist/src/agent/resume-classifier.js.map +1 -1
  51. package/dist/src/agent/spawn/resume.js +0 -18
  52. package/dist/src/agent/spawn/resume.js.map +1 -1
  53. package/dist/src/agent/spawn-env.js +0 -28
  54. package/dist/src/agent/spawn-env.js.map +1 -1
  55. package/dist/src/agent/spawn.js +81 -76
  56. package/dist/src/agent/spawn.js.map +1 -1
  57. package/dist/src/bgtask/presets.js +13 -1
  58. package/dist/src/bgtask/presets.js.map +1 -1
  59. package/dist/src/cli/employee-handler.js +10 -10
  60. package/dist/src/cli/employee-handler.js.map +1 -1
  61. package/dist/src/cli/handlers-completions.js +29 -10
  62. package/dist/src/cli/handlers-completions.js.map +1 -1
  63. package/dist/src/cli/handlers-runtime.js +3 -2
  64. package/dist/src/cli/handlers-runtime.js.map +1 -1
  65. package/dist/src/cli/opencodex-models.js +118 -0
  66. package/dist/src/cli/opencodex-models.js.map +1 -0
  67. package/dist/src/cli/readiness.js +2 -8
  68. package/dist/src/cli/readiness.js.map +1 -1
  69. package/dist/src/cli/registry-live.js +21 -1
  70. package/dist/src/cli/registry-live.js.map +1 -1
  71. package/dist/src/cli/registry.js +10 -21
  72. package/dist/src/cli/registry.js.map +1 -1
  73. package/dist/src/cli/tui/jawcode-bridge.js +15 -13
  74. package/dist/src/cli/tui/jawcode-bridge.js.map +1 -1
  75. package/dist/src/cli/tui/jawcode-render.js +26 -0
  76. package/dist/src/cli/tui/jawcode-render.js.map +1 -1
  77. package/dist/src/cli/tui/render/frame.js +158 -16
  78. package/dist/src/cli/tui/render/frame.js.map +1 -1
  79. package/dist/src/cli/tui/transcript.js +79 -3
  80. package/dist/src/cli/tui/transcript.js.map +1 -1
  81. package/dist/src/core/cli-detect.js +1 -1
  82. package/dist/src/core/cli-detect.js.map +1 -1
  83. package/dist/src/core/employees.js +12 -7
  84. package/dist/src/core/employees.js.map +1 -1
  85. package/dist/src/core/event-bus.js +13 -0
  86. package/dist/src/core/event-bus.js.map +1 -1
  87. package/dist/src/goal/heartbeat.js +8 -0
  88. package/dist/src/goal/heartbeat.js.map +1 -1
  89. package/dist/src/manager/load-timing.js +29 -0
  90. package/dist/src/manager/load-timing.js.map +1 -0
  91. package/dist/src/manager/observability.js.map +1 -1
  92. package/dist/src/manager/server.js +67 -6
  93. package/dist/src/manager/server.js.map +1 -1
  94. package/dist/src/memory/bootstrap.js +43 -1
  95. package/dist/src/memory/bootstrap.js.map +1 -1
  96. package/dist/src/orchestrator/distribute.js +5 -1
  97. package/dist/src/orchestrator/distribute.js.map +1 -1
  98. package/dist/src/orchestrator/state-machine.js +20 -0
  99. package/dist/src/orchestrator/state-machine.js.map +1 -1
  100. package/dist/src/orchestrator/worker-registry.js +2 -1
  101. package/dist/src/orchestrator/worker-registry.js.map +1 -1
  102. package/dist/src/prompt/templates/a1-system.md +61 -11
  103. package/dist/src/prompt/templates/employee.md +13 -0
  104. package/dist/src/prompt/templates/orchestration.md +14 -0
  105. package/dist/src/prompt/templates/worker-context.md +4 -4
  106. package/dist/src/routes/employees.js +8 -8
  107. package/dist/src/routes/employees.js.map +1 -1
  108. package/dist/src/routes/events.js +57 -19
  109. package/dist/src/routes/events.js.map +1 -1
  110. package/dist/src/routes/orchestrate.js +42 -17
  111. package/dist/src/routes/orchestrate.js.map +1 -1
  112. package/dist/src/routes/quota-agy-reverse.js +0 -24
  113. package/dist/src/routes/quota-agy-reverse.js.map +1 -1
  114. package/dist/src/routes/quota.js +0 -145
  115. package/dist/src/routes/quota.js.map +1 -1
  116. package/dist/src/routes/settings.js +2 -7
  117. package/dist/src/routes/settings.js.map +1 -1
  118. package/dist/src/shared/tool-log-sanitize.js +6 -0
  119. package/dist/src/shared/tool-log-sanitize.js.map +1 -1
  120. package/dist/src/telegram/forwarder.js +53 -5
  121. package/dist/src/telegram/forwarder.js.map +1 -1
  122. package/dist/src/trace/store.js +82 -0
  123. package/dist/src/trace/store.js.map +1 -1
  124. package/dist/src/types/cli-engine.js +0 -1
  125. package/dist/src/types/cli-engine.js.map +1 -1
  126. package/dist/src/types/cli-events.js +1 -1
  127. package/dist/src/types/cli-events.js.map +1 -1
  128. package/package.json +2 -2
  129. package/public/dist/assets/Agent-CHCXjXSN.js +1 -0
  130. package/public/dist/assets/CodeCanvas-BzhNLZVF.js +3 -0
  131. package/public/dist/assets/DocPanel-6Z_RELzd.js +1 -0
  132. package/public/dist/assets/{DocPanel-D3FYBhMp.css → DocPanel-DZKPTOm8.css} +1 -1
  133. package/public/dist/assets/FolderPanel-DdlzWAuK.js +3 -0
  134. package/public/dist/assets/{Heartbeat-D-Rfp094.js → Heartbeat-BblY8Z4a.js} +1 -1
  135. package/public/dist/assets/MarkdownRenderer-BivJ4hJm.js +1 -0
  136. package/public/dist/assets/MarkdownRenderer-Cl0oEICj.js +15 -0
  137. package/public/dist/assets/{Memory-VH4ZJCMW.js → Memory-BWcfxzCz.js} +1 -1
  138. package/public/dist/assets/ModelProvider-D4LBX7K2.js +1 -0
  139. package/public/dist/assets/agent-meta-FyI15KAa.js +1 -0
  140. package/public/dist/assets/{app-DaORcAyD.js → app-DszWzakT.js} +4 -4
  141. package/public/dist/assets/bgtask-badge-DZEQ5fMl.js +1 -0
  142. package/public/dist/assets/{bgtask-badge-BtUXEe1T.js → bgtask-badge-b7pj_Dbh.js} +1 -1
  143. package/public/dist/assets/constants-EMUBd5RQ.js +1 -0
  144. package/public/dist/assets/{employees-CZSyvJbx.js → employees-DbgFV33i.js} +1 -1
  145. package/public/dist/assets/{manager-DB5qTw1A.js → manager-CdNOlCyS.js} +4 -4
  146. package/public/dist/assets/{manager-D3waO_XX.css → manager-rHNwUB4e.css} +1 -1
  147. package/public/dist/assets/memory-BT-rs3ag.js +1 -0
  148. package/public/dist/assets/{memory-cWI_Htbn.js → memory-QobGT0Kv.js} +1 -1
  149. package/public/dist/assets/message-history-BY1UNJAO.js +1 -0
  150. package/public/dist/assets/message-history-BmP5JKio.js +142 -0
  151. package/public/dist/assets/{render-Bp8eyqCf.js → render-C3r-9_-J.js} +2 -2
  152. package/public/dist/assets/settings-CJm8ZSJs.js +1 -0
  153. package/public/dist/assets/{settings-DLkP7qRV.js → settings-D70uFh8l.js} +1 -1
  154. package/public/dist/assets/{settings-core-B5XRRNcX.js → settings-core-B2lw2bgO.js} +1 -1
  155. package/public/dist/assets/settings-core-gdS0lo1b.js +1 -0
  156. package/public/dist/assets/{sidebar-CsogFOse.js → sidebar-BqWm2XWP.js} +3 -3
  157. package/public/dist/assets/{skills-Dmf4v9rj.js → skills-CdNOfXlk.js} +1 -1
  158. package/public/dist/assets/skills-DG_CbTEf.js +1 -0
  159. package/public/dist/assets/{slash-commands-vcf7FuPD.js → slash-commands-4Ntm5N7h.js} +1 -1
  160. package/public/dist/assets/slash-commands-B9kOEhJ_.js +1 -0
  161. package/public/dist/assets/{trace-drawer-DuQQ0SNq.js → trace-drawer-DqAwwHHQ.js} +1 -1
  162. package/public/dist/assets/ui-Anw2S64o.js +1 -0
  163. package/public/dist/assets/ui-DsK6r0Xv.js +2 -0
  164. package/public/dist/index.html +1 -1
  165. package/public/dist/manager/index.html +2 -2
  166. package/public/js/constants.ts +7 -7
  167. package/public/js/features/employees.ts +0 -3
  168. package/public/js/features/process-block-dom.ts +6 -1
  169. package/public/js/features/process-block.ts +59 -3
  170. package/public/js/render/file-links.ts +1 -1
  171. package/public/js/ui.ts +9 -1
  172. package/public/js/virtual-scroll.ts +23 -0
  173. package/public/js/ws.ts +7 -2
  174. package/public/manager/src/SidebarRailRouter.tsx +15 -10
  175. package/public/manager/src/code/CodeCanvas.tsx +3 -1
  176. package/public/manager/src/code/CodeTranscript.tsx +5 -4
  177. package/public/manager/src/code/CodeWorkbench.tsx +2 -1
  178. package/public/manager/src/doc-panel/DocPanel.tsx +52 -5
  179. package/public/manager/src/doc-panel/doc-panel.css +10 -0
  180. package/public/manager/src/folder-panel/FolderPanel.tsx +14 -16
  181. package/public/manager/src/folder-panel/use-folder-git-status.ts +15 -0
  182. package/public/manager/src/folder-panel/use-folder-preview-sync.ts +37 -0
  183. package/public/manager/src/folder-panel/use-folder-visible-refresh.ts +19 -6
  184. package/public/manager/src/manager-notes.css +23 -0
  185. package/public/manager/src/notes/rendering/MarkdownRenderer.tsx +80 -2
  186. package/public/manager/src/settings/pages/Agent.tsx +30 -8
  187. package/public/manager/src/settings/pages/ModelProvider.tsx +22 -4
  188. package/public/manager/src/settings/pages/components/agent/AgentEmployeesSection.tsx +5 -1
  189. package/public/manager/src/settings/pages/components/agent/FlushAgentSection.tsx +5 -3
  190. package/public/manager/src/settings/pages/components/agent/RuntimeEmployeeRow.tsx +6 -4
  191. package/public/manager/src/settings/pages/components/agent/RuntimeHeader.tsx +4 -2
  192. package/public/manager/src/settings/pages/components/agent/agent-meta.ts +45 -9
  193. package/public/manager/src/settings/pages/components/agent/runtime-employees-helpers.ts +7 -2
  194. package/scripts/capture-agy-quota-fixture.mjs +138 -0
  195. package/scripts/fresh-install-smoke.ts +1 -1
  196. package/dist/src/agent/events/gemini.js +0 -100
  197. package/dist/src/agent/events/gemini.js.map +0 -1
  198. package/public/dist/assets/Agent-Ci12HWhE.js +0 -1
  199. package/public/dist/assets/CodeCanvas-d1gh0-lY.js +0 -3
  200. package/public/dist/assets/DocPanel-C6XQ7W6A.js +0 -1
  201. package/public/dist/assets/FolderPanel-DIFAqajZ.js +0 -3
  202. package/public/dist/assets/MarkdownRenderer-DXKUEE2a.js +0 -1
  203. package/public/dist/assets/MarkdownRenderer-DcIxpNcX.js +0 -15
  204. package/public/dist/assets/ModelProvider-_tUCghOF.js +0 -1
  205. package/public/dist/assets/agent-meta-DqBTGRRQ.js +0 -1
  206. package/public/dist/assets/bgtask-badge-B3HLOZae.js +0 -1
  207. package/public/dist/assets/constants-DkJ3E63Y.js +0 -1
  208. package/public/dist/assets/memory-BEruUlfu.js +0 -1
  209. package/public/dist/assets/message-history-DU1V9ZAW.js +0 -142
  210. package/public/dist/assets/message-history-tjkyOp_K.js +0 -1
  211. package/public/dist/assets/settings-B-GgyJZI.js +0 -1
  212. package/public/dist/assets/settings-core-Cw1av-Ox.js +0 -1
  213. package/public/dist/assets/skills-CLcoop6W.js +0 -1
  214. package/public/dist/assets/slash-commands-DY6-hzq6.js +0 -1
  215. package/public/dist/assets/ui-BKYuBdZE.js +0 -2
  216. package/public/dist/assets/ui-CqlGwgUo.js +0 -1
@@ -11,6 +11,7 @@ import { RightSidebar } from './panels/RightSidebar';
11
11
  import { BottomPanel, type BottomPanelRenderControls } from './panels/BottomPanel';
12
12
  import { usePanelLayout } from './panels/PanelLayoutProvider';
13
13
  import { currentManagerSurface } from './panels/panel-capabilities';
14
+ import { getDesktop } from './panels/desktop-bridge';
14
15
  import type { RightPanelMode, BottomPanelTab } from './panels/types';
15
16
  import type { FolderPanelSessionState } from './folder-panel/folder-panel-session';
16
17
  import type { WorkbenchRepoRootMode } from './workbench/workbench-resource-types';
@@ -65,7 +66,6 @@ import { ModeSwitch } from './code/ModeSwitch';
65
66
  const CodeCanvas = lazy(() => import('./code/CodeCanvas').then(m => ({ default: m.CodeCanvas })));
66
67
  import './code/code.css';
67
68
  import { useWorkbenchResourceState } from './workbench/useWorkbenchResourceState';
68
- import { WorkerProgressMonitorPanel } from './workers/WorkerProgressMonitorPanel';
69
69
 
70
70
  type WorkspaceSurfaceProps = {
71
71
  active: boolean;
@@ -76,6 +76,12 @@ function WorkspaceSurface(props: WorkspaceSurfaceProps) {
76
76
  return <section className={`workspace-surface${props.active ? ' is-active' : ''}`} hidden={!props.active} aria-hidden={!props.active}>{props.children}</section>;
77
77
  }
78
78
 
79
+ function expandDesktopHomePath(path: string): string {
80
+ if (!path.startsWith('~/')) return path;
81
+ const home = getDesktop()?.getHomePath?.()?.replace(/\/+$/, '');
82
+ return home ? `${home}/${path.slice(2)}` : path;
83
+ }
84
+
79
85
  type Props = {
80
86
  sidebarCollapsed: boolean;
81
87
  activityDockCollapsed: boolean;
@@ -200,7 +206,7 @@ function renderRightPanelContent(
200
206
  onSettingsPatch={onDashboardSettingsPatch}
201
207
  /></Suspense>;
202
208
  case 'folder': return <Suspense fallback={fallback}><FolderPanel selectedFilePath={previewFilePath} externalRootPath={folderRootPath} repoRootPath={repoRootMode === 'instance' ? repoRootPath : null} gitRefreshVersion={gitRefreshVersion} notesTree={notesModel.tree} notesRoot={notesModel.notesRoot} onRootChange={onFolderRootChange} onRepoRootChange={onRepoRootChange} onGitRefresh={onGitRefresh} onPreviewFile={onPreviewFile} sessionState={folderPanelSession} onSessionStateChange={onFolderPanelSessionChange} /></Suspense>;
203
- case 'doc': return <Suspense fallback={fallback}><DocPanel filePath={previewFilePath ?? undefined} /></Suspense>;
209
+ case 'doc': return <Suspense fallback={fallback}><DocPanel filePath={previewFilePath ?? undefined} onOpenLocalFile={onPreviewFile} /></Suspense>;
204
210
  case 'browser': return <Suspense fallback={fallback}><BrowserPanel /></Suspense>;
205
211
  case 'ceo': return jawCeoPanel;
206
212
  default: return null;
@@ -284,7 +290,9 @@ export function SidebarRailRouter(props: Props) {
284
290
  && !props.notesSelectedNote.tags?.includes(props.notesModel.tagFilter),
285
291
  );
286
292
  function handleRightPreviewFile(path: string): void {
287
- setActiveResource(path, 'doc');
293
+ const previewPath = expandDesktopHomePath(path.trim());
294
+ if (!previewPath) return;
295
+ setActiveResource(previewPath, 'doc');
288
296
  panelLayout.dispatch({ type: 'OPEN_RIGHT_PANEL', mode: 'doc', slot: 'bottom' });
289
297
  }
290
298
 
@@ -400,12 +408,9 @@ export function SidebarRailRouter(props: Props) {
400
408
  <div id="code-session-sidebar-host" className="code-session-sidebar-host" />
401
409
  </section>
402
410
  ) : (
403
- <>
404
- <InstanceNavigator active={props.selectedInstance} hiddenCount={props.instances.filter(instance => instance.hidden).length} collapsed={props.sidebarCollapsed}>
405
- {props.instanceListContent}
406
- </InstanceNavigator>
407
- <WorkerProgressMonitorPanel />
408
- </>
411
+ <InstanceNavigator active={props.selectedInstance} hiddenCount={props.instances.filter(instance => instance.hidden).length} collapsed={props.sidebarCollapsed}>
412
+ {props.instanceListContent}
413
+ </InstanceNavigator>
409
414
  )}
410
415
  </div>
411
416
  </>
@@ -434,7 +439,7 @@ export function SidebarRailRouter(props: Props) {
434
439
  {props.viewMode === 'code' && props.sidebarMode === 'instances' ? (
435
440
  <WorkspaceSurface active>
436
441
  <Suspense fallback={<div style={{ padding: '24px', color: 'var(--text-dim)', fontSize: '13px' }}>Loading Code workspace...</div>}>
437
- <CodeCanvas port={props.port} workingDir={codeWorkingDir} onWorkingDirChange={updateRightFolderRoot} />
442
+ <CodeCanvas port={props.port} workingDir={codeWorkingDir} onWorkingDirChange={updateRightFolderRoot} onOpenLocalFile={handleRightPreviewFile} />
438
443
  </Suspense>
439
444
  </WorkspaceSurface>
440
445
  ) : null}
@@ -35,9 +35,10 @@ type CodeCanvasProps = {
35
35
  port: number;
36
36
  workingDir: string;
37
37
  onWorkingDirChange?: (path: string | null) => void;
38
+ onOpenLocalFile?: (path: string) => void;
38
39
  };
39
40
 
40
- export function CodeCanvas({ port, workingDir, onWorkingDirChange }: CodeCanvasProps) {
41
+ export function CodeCanvas({ port, workingDir, onWorkingDirChange, onOpenLocalFile }: CodeCanvasProps) {
41
42
  const client = useMemo(() => createCodeSessionClient(port), [port]);
42
43
  const [codeWorkingDir, setCodeWorkingDir] = useState(workingDir);
43
44
  const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
@@ -588,6 +589,7 @@ export function CodeCanvas({ port, workingDir, onWorkingDirChange }: CodeCanvasP
588
589
  setActivePopup(null);
589
590
  }}
590
591
  onWorkingDirChange={handleWorkingDirChange}
592
+ onOpenLocalFile={onOpenLocalFile}
591
593
  workspaceFrozen={workspaceFrozen}
592
594
  onPickWorkingDir={handlePickWorkspace}
593
595
  />
@@ -9,6 +9,7 @@ type CodeTranscriptProps = {
9
9
  sending: boolean;
10
10
  workingDir: string;
11
11
  transcriptRef: RefObject<HTMLDivElement | null>;
12
+ onOpenLocalFile?: ((path: string) => void) | undefined;
12
13
  };
13
14
 
14
15
  function renderToolContent(content: ToolContent, index: number) {
@@ -107,7 +108,7 @@ function estimateTranscriptRowSize(msg: TranscriptEntry | undefined): number {
107
108
  return 56 + Math.min(420, msg.text.length / 6);
108
109
  }
109
110
 
110
- function renderCodeMessage(msg: TranscriptEntry, i: number) {
111
+ function renderCodeMessage(msg: TranscriptEntry, i: number, onOpenLocalFile?: ((path: string) => void) | undefined) {
111
112
  return (
112
113
  <div key={i} className={`code-message code-message-${msg.role}`}>
113
114
  {msg.role === 'tool' ? (() => {
@@ -165,7 +166,7 @@ function renderCodeMessage(msg: TranscriptEntry, i: number) {
165
166
  <div className="code-message-text">
166
167
  {msg.role === 'assistant' ? (
167
168
  <Suspense fallback={<span>{msg.text}</span>}>
168
- <MarkdownRenderer markdown={msg.text} tableMode="linear" />
169
+ <MarkdownRenderer markdown={msg.text} tableMode="linear" onLocalFileOpen={onOpenLocalFile} />
169
170
  </Suspense>
170
171
  ) : msg.text}
171
172
  </div>
@@ -184,7 +185,7 @@ function renderSendingMessage() {
184
185
  );
185
186
  }
186
187
 
187
- export function CodeTranscript({ messages, sending, workingDir, transcriptRef }: CodeTranscriptProps) {
188
+ export function CodeTranscript({ messages, sending, workingDir, transcriptRef, onOpenLocalFile }: CodeTranscriptProps) {
188
189
  const showSending = sending && messages[messages.length - 1]?.role !== 'assistant';
189
190
  const rowCount = messages.length + (showSending ? 1 : 0);
190
191
  const getItemKey = useCallback((index: number): string | number => {
@@ -248,7 +249,7 @@ export function CodeTranscript({ messages, sending, workingDir, transcriptRef }:
248
249
  data-code-transcript-idx={virtualItem.index}
249
250
  style={{ transform: `translateY(${virtualItem.start}px)` }}
250
251
  >
251
- {msg ? renderCodeMessage(msg, virtualItem.index) : renderSendingMessage()}
252
+ {msg ? renderCodeMessage(msg, virtualItem.index, onOpenLocalFile) : renderSendingMessage()}
252
253
  </div>
253
254
  );
254
255
  })}
@@ -61,6 +61,7 @@ type CodeWorkbenchProps = {
61
61
  onUseModel: (provider: string, model: string) => void | Promise<void>;
62
62
  onUseForNewSessions: (provider: string, model: string) => void | Promise<void>;
63
63
  onWorkingDirChange: (path: string | null) => void;
64
+ onOpenLocalFile?: ((path: string) => void) | undefined;
64
65
  workspaceFrozen: boolean;
65
66
  onPickWorkingDir: () => Promise<string | null>;
66
67
  };
@@ -94,7 +95,7 @@ export function CodeWorkbench(props: CodeWorkbenchProps) {
94
95
  {props.transportState === 'reconnecting' ? 'Live updates reconnecting…' : 'Live updates disconnected — retrying.'}
95
96
  </section>
96
97
  )}
97
- <CodeTranscript messages={props.messages} sending={props.sending} workingDir={props.codeWorkingDir} transcriptRef={props.transcriptRef} />
98
+ <CodeTranscript messages={props.messages} sending={props.sending} workingDir={props.codeWorkingDir} transcriptRef={props.transcriptRef} onOpenLocalFile={props.onOpenLocalFile} />
98
99
  <CodePermissionQueue permissions={props.permissions} onAnswer={props.onPermissionAnswer} />
99
100
  <div className="code-composer-dock">
100
101
  <div className="code-composer-surface" aria-label="Code composer controls">
@@ -9,7 +9,7 @@ import './doc-panel.css';
9
9
  const EXT_LANG: Record<string, string> = {
10
10
  ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
11
11
  py: 'python', rs: 'rust', go: 'go', java: 'java', cpp: 'cpp', c: 'cpp',
12
- css: 'css', html: 'html', xml: 'xml', json: 'json',
12
+ css: 'css', html: 'html', htm: 'html', xml: 'xml', json: 'json',
13
13
  yaml: 'yaml', yml: 'yaml', sh: 'bash', bash: 'bash', sql: 'sql',
14
14
  };
15
15
 
@@ -22,6 +22,10 @@ function isMarkdown(filePath: string): boolean {
22
22
  return /\.(md|mdx)$/i.test(filePath);
23
23
  }
24
24
 
25
+ function isHtml(filePath: string): boolean {
26
+ return /\.html?$/i.test(filePath);
27
+ }
28
+
25
29
  function getFileBridge(): Pick<FolderBridgeApi, 'readFile' | 'getDefaultRoot'> | null {
26
30
  return getDesktop()?.folder ?? null;
27
31
  }
@@ -34,17 +38,60 @@ function getDisplayFileName(filePath: string): string {
34
38
  return filePath.split(/[\\/]/).pop() || filePath;
35
39
  }
36
40
 
37
- function DocContent(props: { filePath: string; content: string; raw: boolean }) {
41
+ function htmlPreviewSrcDoc(content: string): string {
42
+ const csp = [
43
+ "default-src 'none'",
44
+ "img-src data: blob:",
45
+ "style-src 'unsafe-inline' data:",
46
+ "font-src data:",
47
+ "script-src 'none'",
48
+ "connect-src 'none'",
49
+ "object-src 'none'",
50
+ "base-uri 'none'",
51
+ "form-action 'none'",
52
+ "frame-src 'none'",
53
+ ].join('; ');
54
+ return [
55
+ '<!doctype html>',
56
+ '<html>',
57
+ '<head>',
58
+ `<meta http-equiv="Content-Security-Policy" content="${csp}">`,
59
+ '<meta charset="utf-8">',
60
+ '<style>html,body{margin:0;min-height:100%;}body{font-family:system-ui,sans-serif;}</style>',
61
+ '</head>',
62
+ '<body>',
63
+ content,
64
+ '</body>',
65
+ '</html>',
66
+ ].join('');
67
+ }
68
+
69
+ function HtmlPreview(props: { content: string; title: string }) {
70
+ return (
71
+ <iframe
72
+ className="doc-html-preview"
73
+ title={props.title}
74
+ sandbox=""
75
+ referrerPolicy="no-referrer"
76
+ srcDoc={htmlPreviewSrcDoc(props.content)}
77
+ />
78
+ );
79
+ }
80
+
81
+ function DocContent(props: { filePath: string; content: string; raw: boolean; onOpenLocalFile?: ((path: string) => void) | undefined }) {
38
82
  if (props.raw) {
39
83
  return <pre className="doc-pre"><code>{props.content}</code></pre>;
40
84
  }
41
85
  if (isMarkdown(props.filePath)) {
42
86
  return (
43
87
  <article className="notes-preview doc-markdown">
44
- <MarkdownRenderer markdown={props.content} />
88
+ <MarkdownRenderer markdown={props.content} onLocalFileOpen={props.onOpenLocalFile} />
45
89
  </article>
46
90
  );
47
91
  }
92
+ if (isHtml(props.filePath)) {
93
+ return <HtmlPreview content={props.content} title={`Rendered preview of ${getDisplayFileName(props.filePath)}`} />;
94
+ }
48
95
  const lang = getFileLanguage(props.filePath);
49
96
  if (lang) {
50
97
  return <CodeBlock code={props.content} language={lang} />;
@@ -52,7 +99,7 @@ function DocContent(props: { filePath: string; content: string; raw: boolean })
52
99
  return <pre className="doc-pre"><code>{props.content}</code></pre>;
53
100
  }
54
101
 
55
- export function DocPanel(props: { filePath?: string | undefined }) {
102
+ export function DocPanel(props: { filePath?: string | undefined; onOpenLocalFile?: ((path: string) => void) | undefined }) {
56
103
  const bridge = getFileBridge();
57
104
  const scrollRef = useRef<HTMLDivElement | null>(null);
58
105
  const contentBodyRef = useRef<HTMLDivElement | null>(null);
@@ -260,7 +307,7 @@ export function DocPanel(props: { filePath?: string | undefined }) {
260
307
  }}
261
308
  >
262
309
  <div className="doc-content-body" ref={contentBodyRef}>
263
- <DocContent filePath={props.filePath} content={content} raw={raw} />
310
+ <DocContent filePath={props.filePath} content={content} raw={raw} onOpenLocalFile={props.onOpenLocalFile} />
264
311
  </div>
265
312
  </div>
266
313
  </div>
@@ -67,6 +67,8 @@
67
67
  }
68
68
  .doc-content-body {
69
69
  min-width: 0;
70
+ min-height: 100%;
71
+ height: 100%;
70
72
  }
71
73
  .doc-content .notes-mermaid-block.is-loading {
72
74
  min-height: 96px;
@@ -85,6 +87,14 @@
85
87
  .doc-markdown {
86
88
  padding: 12px 16px;
87
89
  }
90
+ .doc-html-preview {
91
+ display: block;
92
+ width: 100%;
93
+ min-height: 100%;
94
+ height: 100%;
95
+ border: 0;
96
+ background: #fff;
97
+ }
88
98
  .doc-content .notes-code-block {
89
99
  margin: 0;
90
100
  border-radius: 0;
@@ -15,6 +15,7 @@ import { useGitWorktrees } from './use-git-worktrees';
15
15
  import { useFolderChord } from './use-folder-chord';
16
16
  import { useFolderSelection, type FolderDragSelection } from './use-folder-selection';
17
17
  import { useFolderVisibleRefresh } from './use-folder-visible-refresh';
18
+ import { useFolderPreviewSync } from './use-folder-preview-sync';
18
19
  import { useFolderWorktreeOperations } from './use-folder-worktree-operations';
19
20
  import { useFolderMutations } from './use-folder-mutations';
20
21
  import { useFolderContextMenu } from './use-folder-context-menu';
@@ -55,6 +56,7 @@ export function FolderPanel(props: FolderPanelProps) {
55
56
  const [actionStatus, setActionStatus] = useState<string | null>(null);
56
57
  const [gitRefreshToken, setGitRefreshToken] = useState(0);
57
58
  const treeRef = useRef<HTMLDivElement | null>(null);
59
+ const restoredRootLoadRef = useRef<string | null>(null);
58
60
  const { folderChordActive, startFolderChord, cancelFolderChord } = useFolderChord();
59
61
  const onPreviewFile = props.onPreviewFile;
60
62
  const selectedFilePath = props.selectedFilePath;
@@ -71,20 +73,13 @@ export function FolderPanel(props: FolderPanelProps) {
71
73
  selectedPaths: folderSelection.selectedPaths,
72
74
  selectOnlyPath: folderSelection.selectOnlyPath,
73
75
  });
74
- useEffect(() => {
75
- const selectedPath = props.selectedFilePath;
76
- if (!selectedPath || !rootPath) return;
77
- if (!isDescendantPath(rootPath, selectedPath)) return;
78
- if (selectedPath === folderSelection.selectedPath) return;
79
- if (!folderSelection.visiblePaths.includes(selectedPath)) return;
80
- folderSelection.selectOnlyPath(selectedPath);
81
- }, [
82
- folderSelection.selectedPath,
83
- folderSelection.selectOnlyPath,
84
- folderSelection.visiblePaths,
85
- props.selectedFilePath,
76
+ useFolderPreviewSync({
77
+ selectedFilePath: props.selectedFilePath,
86
78
  rootPath,
87
- ]);
79
+ selectedPath: folderSelection.selectedPath,
80
+ visiblePaths: folderSelection.visiblePaths,
81
+ selectOnlyPath: folderSelection.selectOnlyPath,
82
+ });
88
83
  useEffect(() => {
89
84
  props.onSessionStateChange?.(folderPanelSessionFromState({
90
85
  rootPath,
@@ -197,13 +192,16 @@ export function FolderPanel(props: FolderPanelProps) {
197
192
 
198
193
  useEffect(() => {
199
194
  const externalRoot = props.externalRootPath;
200
- if (!externalRoot || externalRoot === rootPath) return;
195
+ if (!externalRoot) return;
196
+ if (externalRoot === rootPath) {
197
+ if (entries.length > 0 || restoredRootLoadRef.current === externalRoot) return;
198
+ restoredRootLoadRef.current = externalRoot;
199
+ } else restoredRootLoadRef.current = null;
201
200
  void openFolderRoot(externalRoot);
202
- }, [openFolderRoot, props.externalRootPath, rootPath]);
201
+ }, [entries.length, openFolderRoot, props.externalRootPath, rootPath]);
203
202
 
204
203
  const gitStatus = useFolderGitStatus({
205
204
  rootPath,
206
- repoRoot: repoRootPath,
207
205
  enabled: source.kind === 'electron-folder',
208
206
  refreshToken: gitRefreshToken + gitRefreshVersion,
209
207
  });
@@ -88,6 +88,21 @@ export function useFolderGitStatus(input: UseFolderGitStatusInput): FolderPanelG
88
88
  setState(stateFromStatus(result.status));
89
89
  return;
90
90
  }
91
+ if (repoRoot && /repo root mismatch/i.test(result.error)) {
92
+ const healed = await loadFolderGitStatus(rootPath, {
93
+ includeIgnored: true,
94
+ includeUntracked: true,
95
+ });
96
+ if (cancelled) return;
97
+ if (healed.ok) {
98
+ setState(stateFromStatus(healed.status));
99
+ return;
100
+ }
101
+ setState(healed.quiet
102
+ ? EMPTY_FOLDER_GIT_STATE
103
+ : { ...EMPTY_FOLDER_GIT_STATE, error: healed.error });
104
+ return;
105
+ }
91
106
  setState(result.quiet
92
107
  ? EMPTY_FOLDER_GIT_STATE
93
108
  : { ...EMPTY_FOLDER_GIT_STATE, error: result.error });
@@ -0,0 +1,37 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { isDescendantPath } from './folder-panel-state';
3
+
4
+ type UseFolderPreviewSyncInput = {
5
+ selectedFilePath: string | null | undefined;
6
+ rootPath: string | null;
7
+ selectedPath: string | null;
8
+ visiblePaths: string[];
9
+ selectOnlyPath: (path: string | null) => void;
10
+ };
11
+
12
+ /**
13
+ * Mirrors the externally-driven preview file (DocPanel/DiffPanel selection) into
14
+ * the folder tree's visible-row selection, WITHOUT clobbering local row clicks.
15
+ *
16
+ * The sync only fires when the external preview path actually changes. A local
17
+ * row click changes `selectedPath` but not `selectedFilePath`, so the guard
18
+ * leaves the user's selection untouched (the 990461c1 selection-lock fix). When
19
+ * the preview path is cleared/changes to a non-visible or out-of-root file, the
20
+ * current selection is left as-is rather than being reset.
21
+ */
22
+ export function useFolderPreviewSync(input: UseFolderPreviewSyncInput): void {
23
+ const { selectedFilePath, rootPath, selectedPath, visiblePaths, selectOnlyPath } = input;
24
+ const syncedPreviewPathRef = useRef<string | null>(null);
25
+
26
+ useEffect(() => {
27
+ const previewPath = selectedFilePath ?? null;
28
+ const previewPathChanged = previewPath !== syncedPreviewPathRef.current;
29
+ if (previewPathChanged) syncedPreviewPathRef.current = previewPath;
30
+ if (!previewPathChanged && selectedPath) return;
31
+ if (!previewPath || !rootPath) return;
32
+ if (!isDescendantPath(rootPath, previewPath)) return;
33
+ if (previewPath === selectedPath) return;
34
+ if (!visiblePaths.includes(previewPath)) return;
35
+ selectOnlyPath(previewPath);
36
+ }, [rootPath, selectOnlyPath, selectedFilePath, selectedPath, visiblePaths]);
37
+ }
@@ -57,6 +57,12 @@ export function useFolderVisibleRefresh(input: UseFolderVisibleRefreshInput) {
57
57
  const refreshingRef = useRef(false);
58
58
  const queuedRefreshRef = useRef<{ reason: FolderRefreshReason; options: FolderVisibleRefreshOptions } | null>(null);
59
59
  const watchTimerRef = useRef<number | null>(null);
60
+ const runRefreshRef = useRef<(reason: FolderRefreshReason, options?: FolderVisibleRefreshOptions) => Promise<void>>(async () => undefined);
61
+ const mountedRef = useRef(true);
62
+ useEffect(() => {
63
+ mountedRef.current = true;
64
+ return () => { mountedRef.current = false; };
65
+ }, []);
60
66
 
61
67
  const clearWatchTimer = useCallback(() => {
62
68
  if (watchTimerRef.current === null) return;
@@ -82,8 +88,10 @@ export function useFolderVisibleRefresh(input: UseFolderVisibleRefreshInput) {
82
88
  return;
83
89
  }
84
90
  refreshingRef.current = true;
85
- setIsRefreshing(true);
86
- setRefreshStatus(reason === 'watch' ? 'Refreshing changed files...' : 'Refreshing folder...');
91
+ if (mountedRef.current) {
92
+ setIsRefreshing(true);
93
+ setRefreshStatus(reason === 'watch' ? 'Refreshing changed files...' : 'Refreshing folder...');
94
+ }
87
95
  try {
88
96
  const expandedPaths = Array.from(new Set([...Array.from(expanded), ...(options.extraPaths ?? [])]))
89
97
  .slice(0, MAX_EXPANDED_REFRESH_BRANCHES);
@@ -92,16 +100,19 @@ export function useFolderVisibleRefresh(input: UseFolderVisibleRefreshInput) {
92
100
  bumpGitRefresh();
93
101
  onGitRefresh?.();
94
102
  refreshWorktrees();
103
+ if (!mountedRef.current) return;
95
104
  const skippedCount = Math.max(0, expanded.size - expandedPaths.length);
96
105
  setRefreshStatus(skippedCount > 0
97
106
  ? `${reasonLabel(reason)}; ${skippedCount} collapsed/overflow branches skipped`
98
107
  : reasonLabel(reason));
99
108
  } finally {
100
109
  refreshingRef.current = false;
101
- setIsRefreshing(false);
110
+ if (mountedRef.current) setIsRefreshing(false);
102
111
  const queuedRefresh = queuedRefreshRef.current;
103
112
  queuedRefreshRef.current = null;
104
- if (queuedRefresh) void runRefresh(queuedRefresh.reason, queuedRefresh.options);
113
+ if (queuedRefresh && mountedRef.current) {
114
+ void runRefreshRef.current(queuedRefresh.reason, queuedRefresh.options);
115
+ }
105
116
  }
106
117
  }, [bumpGitRefresh, expanded, loadChildren, loadDir, onGitRefresh, refreshWorktrees, rootPath]);
107
118
 
@@ -112,13 +123,15 @@ export function useFolderVisibleRefresh(input: UseFolderVisibleRefreshInput) {
112
123
  await runRefresh(reason, options);
113
124
  }, [runRefresh]);
114
125
 
126
+ useEffect(() => { runRefreshRef.current = runRefresh; }, [runRefresh]);
127
+
115
128
  const scheduleWatchRefresh = useCallback(() => {
116
129
  clearWatchTimer();
117
130
  watchTimerRef.current = window.setTimeout(() => {
118
131
  watchTimerRef.current = null;
119
- void runRefresh('watch');
132
+ void runRefreshRef.current('watch');
120
133
  }, WATCH_REFRESH_DELAY_MS);
121
- }, [clearWatchTimer, runRefresh]);
134
+ }, [clearWatchTimer]);
122
135
 
123
136
  useEffect(() => {
124
137
  if (!source.watchDir || !source.onDirChange || rootPath === null) return;
@@ -1142,6 +1142,29 @@
1142
1142
  font-size: 0.92em;
1143
1143
  }
1144
1144
 
1145
+ .markdown-local-file-link {
1146
+ display: inline;
1147
+ border: 0;
1148
+ padding: 0;
1149
+ background: transparent;
1150
+ color: var(--accent-default);
1151
+ font: inherit;
1152
+ text-decoration: underline;
1153
+ text-decoration-thickness: 1px;
1154
+ text-underline-offset: 2px;
1155
+ cursor: pointer;
1156
+ }
1157
+
1158
+ .markdown-local-file-link:hover {
1159
+ color: var(--accent);
1160
+ }
1161
+
1162
+ .markdown-local-file-link:focus-visible {
1163
+ outline: 2px solid var(--accent);
1164
+ outline-offset: 2px;
1165
+ border-radius: 3px;
1166
+ }
1167
+
1145
1168
  .notes-preview :where(.katex-display, .notes-code-block, .notes-mermaid-block) {
1146
1169
  max-width: 100%;
1147
1170
  overflow-x: auto;
@@ -1,4 +1,5 @@
1
1
  import { Children, createElement, isValidElement, useMemo } from 'react';
2
+ import type { MouseEvent } from 'react';
2
3
  import type { ComponentProps, CSSProperties, ReactNode } from 'react';
3
4
  import ReactMarkdown from 'react-markdown';
4
5
  import type { Components } from 'react-markdown';
@@ -24,6 +25,7 @@ type MarkdownRendererProps = {
24
25
  outgoing?: NotesNoteLinkRef[] | undefined;
25
26
  notes?: readonly NotesNoteMetadata[] | undefined;
26
27
  onWikiLinkNavigate?: ((path: string) => void) | undefined;
28
+ onLocalFileOpen?: ((path: string) => void) | undefined;
27
29
  tableMode?: 'semantic' | 'linear' | undefined;
28
30
  };
29
31
 
@@ -114,6 +116,77 @@ function languageFromCodeNode(node: ReactNode): string {
114
116
  }
115
117
 
116
118
  const NOOP_NAVIGATE = (_path: string): void => {};
119
+ const LOCAL_FILE_PATH_RE_G = /(?:~\/[^\s)`\]"'<>]+|\/(?:Users|home|tmp|var|opt|private)\/[^\s)`\]"'<>]+)/g;
120
+ const TRAILING_PATH_PUNCT_RE = /[.,!?:;]+$/;
121
+
122
+ function isLocalFilePathCandidate(path: string): boolean {
123
+ return path.startsWith('~/') || /^\/(?:Users|home|tmp|var|opt|private)\//.test(path);
124
+ }
125
+
126
+ function localPathLabel(path: string): string {
127
+ const basename = path.split('/').pop() || path;
128
+ return /\.\w{1,10}$/.test(basename) ? basename : path;
129
+ }
130
+
131
+ function splitTextWithLocalFileLinks(
132
+ text: string,
133
+ onLocalFileOpen: ((path: string) => void) | undefined,
134
+ keyPrefix: string,
135
+ ): ReactNode[] {
136
+ if (!text || !onLocalFileOpen) return [text];
137
+ LOCAL_FILE_PATH_RE_G.lastIndex = 0;
138
+ const out: ReactNode[] = [];
139
+ let cursor = 0;
140
+ let index = 0;
141
+ let match: RegExpExecArray | null;
142
+ while ((match = LOCAL_FILE_PATH_RE_G.exec(text)) !== null) {
143
+ const raw = match[0];
144
+ const clean = raw.replace(TRAILING_PATH_PUNCT_RE, '');
145
+ if (!isLocalFilePathCandidate(clean)) continue;
146
+ if (match.index > cursor) out.push(text.slice(cursor, match.index));
147
+ const trailing = raw.slice(clean.length);
148
+ out.push(
149
+ createElement(
150
+ 'button',
151
+ {
152
+ key: `${keyPrefix}-lf-${index++}`,
153
+ type: 'button',
154
+ className: 'markdown-local-file-link',
155
+ title: clean,
156
+ onClick: (event: MouseEvent<HTMLButtonElement>) => {
157
+ event.preventDefault();
158
+ onLocalFileOpen(clean);
159
+ },
160
+ },
161
+ localPathLabel(clean),
162
+ ),
163
+ );
164
+ if (trailing) out.push(trailing);
165
+ cursor = match.index + raw.length;
166
+ }
167
+ if (cursor === 0) return [text];
168
+ if (cursor < text.length) out.push(text.slice(cursor));
169
+ return out;
170
+ }
171
+
172
+ function splitChildrenWithLocalFileLinks(
173
+ children: ReactNode,
174
+ onLocalFileOpen: ((path: string) => void) | undefined,
175
+ keyPrefix: string,
176
+ ): ReactNode {
177
+ if (!onLocalFileOpen) return children;
178
+ const out: ReactNode[] = [];
179
+ let stringIndex = 0;
180
+ Children.forEach(children, (child, i) => {
181
+ if (typeof child === 'string') {
182
+ const segments = splitTextWithLocalFileLinks(child, onLocalFileOpen, `${keyPrefix}-${stringIndex++}-${i}`);
183
+ for (const seg of segments) out.push(seg);
184
+ return;
185
+ }
186
+ out.push(child);
187
+ });
188
+ return out;
189
+ }
117
190
 
118
191
  export function MarkdownRenderer(props: MarkdownRendererProps) {
119
192
  const renderedMarkdown = useMemo(
@@ -128,15 +201,20 @@ export function MarkdownRenderer(props: MarkdownRendererProps) {
128
201
  onNavigate: props.onWikiLinkNavigate ?? NOOP_NAVIGATE,
129
202
  }), [props.outgoing, props.notes, props.onWikiLinkNavigate]);
130
203
 
204
+ const inlineTransform = (children: ReactNode, keyPrefix: string): ReactNode => {
205
+ const withWikiLinks = splitChildrenWithWikiLinks(children, wikiCtx, keyPrefix);
206
+ return splitChildrenWithLocalFileLinks(withWikiLinks, props.onLocalFileOpen, keyPrefix);
207
+ };
208
+
131
209
  const wikiTransform = (tag: WikiContainerTag) => (containerProps: WikiContainerProps) => {
132
210
  const { children, node: _node, className, id, style } = containerProps;
133
- const transformed = splitChildrenWithWikiLinks(children, wikiCtx, tag);
211
+ const transformed = inlineTransform(children, tag);
134
212
  return createElement(tag, { className, id, style }, transformed);
135
213
  };
136
214
 
137
215
  const linearCellTransform = (tag: 'td' | 'th') => (cellProps: LinearTableCellProps) => {
138
216
  const { children, node: _node, className, style, align } = cellProps;
139
- const transformed = splitChildrenWithWikiLinks(children, wikiCtx, tag);
217
+ const transformed = inlineTransform(children, tag);
140
218
  return (
141
219
  <span
142
220
  className={mergeClassName(`markdown-linear-table-cell markdown-linear-table-${tag}`, className)}