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.
- package/README.ja.md +1 -4
- package/README.ko.md +1 -4
- package/README.md +4 -7
- package/README.zh-CN.md +1 -4
- package/dist/bin/commands/bgtask.js +3 -1
- package/dist/bin/commands/bgtask.js.map +1 -1
- package/dist/bin/commands/chat.js +2 -2
- package/dist/bin/commands/chat.js.map +1 -1
- package/dist/bin/commands/dispatch.js +10 -3
- package/dist/bin/commands/dispatch.js.map +1 -1
- package/dist/bin/commands/tui/fullscreen-mode.js +44 -11
- package/dist/bin/commands/tui/fullscreen-mode.js.map +1 -1
- package/dist/bin/commands/tui/ws-handler.js +15 -1
- package/dist/bin/commands/tui/ws-handler.js.map +1 -1
- package/dist/scripts/fresh-install-smoke.js +1 -1
- package/dist/scripts/fresh-install-smoke.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/src/agent/agy-bootstrap.js +104 -17
- package/dist/src/agent/agy-bootstrap.js.map +1 -1
- package/dist/src/agent/agy-runtime.js +104 -0
- package/dist/src/agent/agy-runtime.js.map +1 -1
- package/dist/src/agent/agy-transcript-watcher.js +14 -0
- package/dist/src/agent/agy-transcript-watcher.js.map +1 -1
- package/dist/src/agent/args.js +16 -83
- package/dist/src/agent/args.js.map +1 -1
- package/dist/src/agent/error-classifier.js +1 -3
- package/dist/src/agent/error-classifier.js.map +1 -1
- package/dist/src/agent/events/claude.js +60 -3
- package/dist/src/agent/events/claude.js.map +1 -1
- package/dist/src/agent/events/helpers.js +1 -1
- package/dist/src/agent/events/helpers.js.map +1 -1
- package/dist/src/agent/events/index.js +27 -27
- package/dist/src/agent/events/index.js.map +1 -1
- package/dist/src/agent/events/summary.js +0 -21
- package/dist/src/agent/events/summary.js.map +1 -1
- package/dist/src/agent/events/tool-labels.js +0 -28
- package/dist/src/agent/events/tool-labels.js.map +1 -1
- package/dist/src/agent/jwc-event-mapper.js +3 -3
- package/dist/src/agent/jwc-event-mapper.js.map +1 -1
- package/dist/src/agent/jwc-runtime.js +5 -1
- package/dist/src/agent/jwc-runtime.js.map +1 -1
- package/dist/src/agent/kiro-runtime.js +40 -14
- package/dist/src/agent/kiro-runtime.js.map +1 -1
- package/dist/src/agent/lifecycle-handler.js +2 -77
- package/dist/src/agent/lifecycle-handler.js.map +1 -1
- package/dist/src/agent/prompt-context.js +38 -0
- package/dist/src/agent/prompt-context.js.map +1 -0
- package/dist/src/agent/resume-classifier.js +0 -4
- package/dist/src/agent/resume-classifier.js.map +1 -1
- package/dist/src/agent/spawn/resume.js +0 -18
- package/dist/src/agent/spawn/resume.js.map +1 -1
- package/dist/src/agent/spawn-env.js +0 -28
- package/dist/src/agent/spawn-env.js.map +1 -1
- package/dist/src/agent/spawn.js +81 -76
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/bgtask/presets.js +13 -1
- package/dist/src/bgtask/presets.js.map +1 -1
- package/dist/src/cli/employee-handler.js +10 -10
- package/dist/src/cli/employee-handler.js.map +1 -1
- package/dist/src/cli/handlers-completions.js +29 -10
- package/dist/src/cli/handlers-completions.js.map +1 -1
- package/dist/src/cli/handlers-runtime.js +3 -2
- package/dist/src/cli/handlers-runtime.js.map +1 -1
- package/dist/src/cli/opencodex-models.js +118 -0
- package/dist/src/cli/opencodex-models.js.map +1 -0
- package/dist/src/cli/readiness.js +2 -8
- package/dist/src/cli/readiness.js.map +1 -1
- package/dist/src/cli/registry-live.js +21 -1
- package/dist/src/cli/registry-live.js.map +1 -1
- package/dist/src/cli/registry.js +10 -21
- package/dist/src/cli/registry.js.map +1 -1
- package/dist/src/cli/tui/jawcode-bridge.js +15 -13
- package/dist/src/cli/tui/jawcode-bridge.js.map +1 -1
- package/dist/src/cli/tui/jawcode-render.js +26 -0
- package/dist/src/cli/tui/jawcode-render.js.map +1 -1
- package/dist/src/cli/tui/render/frame.js +158 -16
- package/dist/src/cli/tui/render/frame.js.map +1 -1
- package/dist/src/cli/tui/transcript.js +79 -3
- package/dist/src/cli/tui/transcript.js.map +1 -1
- package/dist/src/core/cli-detect.js +1 -1
- package/dist/src/core/cli-detect.js.map +1 -1
- package/dist/src/core/employees.js +12 -7
- package/dist/src/core/employees.js.map +1 -1
- package/dist/src/core/event-bus.js +13 -0
- package/dist/src/core/event-bus.js.map +1 -1
- package/dist/src/goal/heartbeat.js +8 -0
- package/dist/src/goal/heartbeat.js.map +1 -1
- package/dist/src/manager/load-timing.js +29 -0
- package/dist/src/manager/load-timing.js.map +1 -0
- package/dist/src/manager/observability.js.map +1 -1
- package/dist/src/manager/server.js +67 -6
- package/dist/src/manager/server.js.map +1 -1
- package/dist/src/memory/bootstrap.js +43 -1
- package/dist/src/memory/bootstrap.js.map +1 -1
- package/dist/src/orchestrator/distribute.js +5 -1
- package/dist/src/orchestrator/distribute.js.map +1 -1
- package/dist/src/orchestrator/state-machine.js +20 -0
- package/dist/src/orchestrator/state-machine.js.map +1 -1
- package/dist/src/orchestrator/worker-registry.js +2 -1
- package/dist/src/orchestrator/worker-registry.js.map +1 -1
- package/dist/src/prompt/templates/a1-system.md +61 -11
- package/dist/src/prompt/templates/employee.md +13 -0
- package/dist/src/prompt/templates/orchestration.md +14 -0
- package/dist/src/prompt/templates/worker-context.md +4 -4
- package/dist/src/routes/employees.js +8 -8
- package/dist/src/routes/employees.js.map +1 -1
- package/dist/src/routes/events.js +57 -19
- package/dist/src/routes/events.js.map +1 -1
- package/dist/src/routes/orchestrate.js +42 -17
- package/dist/src/routes/orchestrate.js.map +1 -1
- package/dist/src/routes/quota-agy-reverse.js +0 -24
- package/dist/src/routes/quota-agy-reverse.js.map +1 -1
- package/dist/src/routes/quota.js +0 -145
- package/dist/src/routes/quota.js.map +1 -1
- package/dist/src/routes/settings.js +2 -7
- package/dist/src/routes/settings.js.map +1 -1
- package/dist/src/shared/tool-log-sanitize.js +6 -0
- package/dist/src/shared/tool-log-sanitize.js.map +1 -1
- package/dist/src/telegram/forwarder.js +53 -5
- package/dist/src/telegram/forwarder.js.map +1 -1
- package/dist/src/trace/store.js +82 -0
- package/dist/src/trace/store.js.map +1 -1
- package/dist/src/types/cli-engine.js +0 -1
- package/dist/src/types/cli-engine.js.map +1 -1
- package/dist/src/types/cli-events.js +1 -1
- package/dist/src/types/cli-events.js.map +1 -1
- package/package.json +2 -2
- package/public/dist/assets/Agent-CHCXjXSN.js +1 -0
- package/public/dist/assets/CodeCanvas-BzhNLZVF.js +3 -0
- package/public/dist/assets/DocPanel-6Z_RELzd.js +1 -0
- package/public/dist/assets/{DocPanel-D3FYBhMp.css → DocPanel-DZKPTOm8.css} +1 -1
- package/public/dist/assets/FolderPanel-DdlzWAuK.js +3 -0
- package/public/dist/assets/{Heartbeat-D-Rfp094.js → Heartbeat-BblY8Z4a.js} +1 -1
- package/public/dist/assets/MarkdownRenderer-BivJ4hJm.js +1 -0
- package/public/dist/assets/MarkdownRenderer-Cl0oEICj.js +15 -0
- package/public/dist/assets/{Memory-VH4ZJCMW.js → Memory-BWcfxzCz.js} +1 -1
- package/public/dist/assets/ModelProvider-D4LBX7K2.js +1 -0
- package/public/dist/assets/agent-meta-FyI15KAa.js +1 -0
- package/public/dist/assets/{app-DaORcAyD.js → app-DszWzakT.js} +4 -4
- package/public/dist/assets/bgtask-badge-DZEQ5fMl.js +1 -0
- package/public/dist/assets/{bgtask-badge-BtUXEe1T.js → bgtask-badge-b7pj_Dbh.js} +1 -1
- package/public/dist/assets/constants-EMUBd5RQ.js +1 -0
- package/public/dist/assets/{employees-CZSyvJbx.js → employees-DbgFV33i.js} +1 -1
- package/public/dist/assets/{manager-DB5qTw1A.js → manager-CdNOlCyS.js} +4 -4
- package/public/dist/assets/{manager-D3waO_XX.css → manager-rHNwUB4e.css} +1 -1
- package/public/dist/assets/memory-BT-rs3ag.js +1 -0
- package/public/dist/assets/{memory-cWI_Htbn.js → memory-QobGT0Kv.js} +1 -1
- package/public/dist/assets/message-history-BY1UNJAO.js +1 -0
- package/public/dist/assets/message-history-BmP5JKio.js +142 -0
- package/public/dist/assets/{render-Bp8eyqCf.js → render-C3r-9_-J.js} +2 -2
- package/public/dist/assets/settings-CJm8ZSJs.js +1 -0
- package/public/dist/assets/{settings-DLkP7qRV.js → settings-D70uFh8l.js} +1 -1
- package/public/dist/assets/{settings-core-B5XRRNcX.js → settings-core-B2lw2bgO.js} +1 -1
- package/public/dist/assets/settings-core-gdS0lo1b.js +1 -0
- package/public/dist/assets/{sidebar-CsogFOse.js → sidebar-BqWm2XWP.js} +3 -3
- package/public/dist/assets/{skills-Dmf4v9rj.js → skills-CdNOfXlk.js} +1 -1
- package/public/dist/assets/skills-DG_CbTEf.js +1 -0
- package/public/dist/assets/{slash-commands-vcf7FuPD.js → slash-commands-4Ntm5N7h.js} +1 -1
- package/public/dist/assets/slash-commands-B9kOEhJ_.js +1 -0
- package/public/dist/assets/{trace-drawer-DuQQ0SNq.js → trace-drawer-DqAwwHHQ.js} +1 -1
- package/public/dist/assets/ui-Anw2S64o.js +1 -0
- package/public/dist/assets/ui-DsK6r0Xv.js +2 -0
- package/public/dist/index.html +1 -1
- package/public/dist/manager/index.html +2 -2
- package/public/js/constants.ts +7 -7
- package/public/js/features/employees.ts +0 -3
- package/public/js/features/process-block-dom.ts +6 -1
- package/public/js/features/process-block.ts +59 -3
- package/public/js/render/file-links.ts +1 -1
- package/public/js/ui.ts +9 -1
- package/public/js/virtual-scroll.ts +23 -0
- package/public/js/ws.ts +7 -2
- package/public/manager/src/SidebarRailRouter.tsx +15 -10
- package/public/manager/src/code/CodeCanvas.tsx +3 -1
- package/public/manager/src/code/CodeTranscript.tsx +5 -4
- package/public/manager/src/code/CodeWorkbench.tsx +2 -1
- package/public/manager/src/doc-panel/DocPanel.tsx +52 -5
- package/public/manager/src/doc-panel/doc-panel.css +10 -0
- package/public/manager/src/folder-panel/FolderPanel.tsx +14 -16
- package/public/manager/src/folder-panel/use-folder-git-status.ts +15 -0
- package/public/manager/src/folder-panel/use-folder-preview-sync.ts +37 -0
- package/public/manager/src/folder-panel/use-folder-visible-refresh.ts +19 -6
- package/public/manager/src/manager-notes.css +23 -0
- package/public/manager/src/notes/rendering/MarkdownRenderer.tsx +80 -2
- package/public/manager/src/settings/pages/Agent.tsx +30 -8
- package/public/manager/src/settings/pages/ModelProvider.tsx +22 -4
- package/public/manager/src/settings/pages/components/agent/AgentEmployeesSection.tsx +5 -1
- package/public/manager/src/settings/pages/components/agent/FlushAgentSection.tsx +5 -3
- package/public/manager/src/settings/pages/components/agent/RuntimeEmployeeRow.tsx +6 -4
- package/public/manager/src/settings/pages/components/agent/RuntimeHeader.tsx +4 -2
- package/public/manager/src/settings/pages/components/agent/agent-meta.ts +45 -9
- package/public/manager/src/settings/pages/components/agent/runtime-employees-helpers.ts +7 -2
- package/scripts/capture-agy-quota-fixture.mjs +138 -0
- package/scripts/fresh-install-smoke.ts +1 -1
- package/dist/src/agent/events/gemini.js +0 -100
- package/dist/src/agent/events/gemini.js.map +0 -1
- package/public/dist/assets/Agent-Ci12HWhE.js +0 -1
- package/public/dist/assets/CodeCanvas-d1gh0-lY.js +0 -3
- package/public/dist/assets/DocPanel-C6XQ7W6A.js +0 -1
- package/public/dist/assets/FolderPanel-DIFAqajZ.js +0 -3
- package/public/dist/assets/MarkdownRenderer-DXKUEE2a.js +0 -1
- package/public/dist/assets/MarkdownRenderer-DcIxpNcX.js +0 -15
- package/public/dist/assets/ModelProvider-_tUCghOF.js +0 -1
- package/public/dist/assets/agent-meta-DqBTGRRQ.js +0 -1
- package/public/dist/assets/bgtask-badge-B3HLOZae.js +0 -1
- package/public/dist/assets/constants-DkJ3E63Y.js +0 -1
- package/public/dist/assets/memory-BEruUlfu.js +0 -1
- package/public/dist/assets/message-history-DU1V9ZAW.js +0 -142
- package/public/dist/assets/message-history-tjkyOp_K.js +0 -1
- package/public/dist/assets/settings-B-GgyJZI.js +0 -1
- package/public/dist/assets/settings-core-Cw1av-Ox.js +0 -1
- package/public/dist/assets/skills-CLcoop6W.js +0 -1
- package/public/dist/assets/slash-commands-DY6-hzq6.js +0 -1
- package/public/dist/assets/ui-BKYuBdZE.js +0 -2
- 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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
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
|
|
132
|
+
void runRefreshRef.current('watch');
|
|
120
133
|
}, WATCH_REFRESH_DELAY_MS);
|
|
121
|
-
}, [clearWatchTimer
|
|
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 =
|
|
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 =
|
|
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)}
|