march-cli 0.1.34 → 0.1.36
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/package.json +12 -1
- package/src/agent/code-search/cache.mjs +133 -0
- package/src/agent/code-search/chunk-rules.mjs +107 -0
- package/src/agent/code-search/chunker.mjs +125 -0
- package/src/agent/code-search/engine.mjs +109 -0
- package/src/agent/code-search/languages.mjs +25 -0
- package/src/agent/code-search/parser-pool.mjs +29 -0
- package/src/agent/code-search/rerank.mjs +43 -0
- package/src/agent/code-search/retrieval/bm25.mjs +47 -0
- package/src/agent/code-search/retrieval/fusion.mjs +18 -0
- package/src/agent/code-search/retrieval/model2vec.mjs +96 -0
- package/src/agent/code-search/retrieval/safetensors.mjs +49 -0
- package/src/agent/code-search/retrieval/vector.mjs +107 -0
- package/src/agent/code-search/retrieval/wordpiece.mjs +82 -0
- package/src/agent/code-search/scanner.mjs +84 -0
- package/src/agent/code-search/tokenize.mjs +16 -0
- package/src/agent/code-search/tool.mjs +75 -0
- package/src/agent/lifecycle/runner-lifecycle.mjs +16 -0
- package/src/agent/lifecycle/runtime-restart-tool.mjs +22 -0
- package/src/agent/runner/provider-quota-runtime.mjs +38 -0
- package/src/agent/runner.mjs +14 -14
- package/src/agent/runtime/remote-runner-client.mjs +9 -15
- package/src/agent/runtime/runner-ipc-target.mjs +10 -22
- package/src/agent/runtime/runner-process-client.mjs +101 -24
- package/src/agent/runtime/runner-runtime-host.mjs +2 -0
- package/src/agent/runtime/state/runner-state.mjs +81 -0
- package/src/agent/runtime/ui-event-bridge.mjs +2 -0
- package/src/agent/session/session-options.mjs +2 -1
- package/src/agent/tools.mjs +6 -1
- package/src/cli/args.mjs +14 -3
- package/src/cli/commands/catalog/visible-commands.mjs +5 -0
- package/src/cli/commands/help-command.mjs +1 -7
- package/src/cli/commands/registry/slash-command-registry.mjs +296 -0
- package/src/cli/commands/status-command.mjs +61 -35
- package/src/cli/input/autocomplete.mjs +2 -25
- package/src/cli/repl-loop.mjs +24 -41
- package/src/cli/slash-commands.mjs +19 -185
- package/src/cli/startup/app-runtime.mjs +201 -0
- package/src/cli/startup/configured-command.mjs +9 -0
- package/src/cli/startup/early-command.mjs +29 -0
- package/src/cli/turn/turn-input-preparer.mjs +41 -0
- package/src/context/system-core/base.md +5 -0
- package/src/main.mjs +47 -242
- package/src/provider/quota/codex.mjs +278 -0
- package/src/provider/quota/index.mjs +46 -0
- package/src/provider/quota/transport-observer.mjs +99 -0
- package/src/web-ui/command.mjs +112 -0
- package/src/web-ui/index.html +12 -0
- package/src/web-ui/runtime-host.mjs +188 -0
- package/src/web-ui/server.mjs +140 -0
- package/src/web-ui/session-manager.mjs +111 -0
- package/src/web-ui/src/App.tsx +7 -0
- package/src/web-ui/src/components/AppShell.tsx +48 -0
- package/src/web-ui/src/components/Composer.tsx +47 -0
- package/src/web-ui/src/components/FileExplorer.tsx +46 -0
- package/src/web-ui/src/components/RightSidebar.tsx +115 -0
- package/src/web-ui/src/components/SessionTimeline.tsx +31 -0
- package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +109 -0
- package/src/web-ui/src/components/timeline/TimelineList.tsx +14 -0
- package/src/web-ui/src/fileTreeAdapter.ts +51 -0
- package/src/web-ui/src/main.tsx +11 -0
- package/src/web-ui/src/mockData.ts +87 -0
- package/src/web-ui/src/model.ts +82 -0
- package/src/web-ui/src/runtime/client.ts +81 -0
- package/src/web-ui/src/runtime/runtimeTimeline.ts +88 -0
- package/src/web-ui/src/runtime/useWebRuntime.ts +144 -0
- package/src/web-ui/src/styles/shell.css +166 -0
- package/src/web-ui/src/styles/tokens.css +116 -0
- package/src/web-ui/src/timelineAdapter.ts +43 -0
- package/src/web-ui/src/vite-env.d.ts +1 -0
- package/src/web-ui/tsconfig.json +20 -0
- package/src/web-ui/vite.config.mjs +11 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { WebRuntimeState } from "../runtime/useWebRuntime";
|
|
3
|
+
import { Composer } from "./Composer";
|
|
4
|
+
import { FileExplorer } from "./FileExplorer";
|
|
5
|
+
import { RightSidebar } from "./RightSidebar";
|
|
6
|
+
import { SessionTimeline } from "./SessionTimeline";
|
|
7
|
+
|
|
8
|
+
export type AppShellProps = {
|
|
9
|
+
runtime: WebRuntimeState;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function AppShell({ runtime }: AppShellProps) {
|
|
13
|
+
const [leftOpen, setLeftOpen] = useState(false);
|
|
14
|
+
const [rightOpen, setRightOpen] = useState(false);
|
|
15
|
+
const { model } = runtime;
|
|
16
|
+
const closePanels = () => {
|
|
17
|
+
setLeftOpen(false);
|
|
18
|
+
setRightOpen(false);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="app-shell" data-left-open={leftOpen} data-right-open={rightOpen}>
|
|
23
|
+
<div className="overlay" onClick={closePanels} />
|
|
24
|
+
<FileExplorer root={model.workspace} />
|
|
25
|
+
<SessionTimeline timeline={model.timeline} connected={runtime.connected} error={runtime.error} />
|
|
26
|
+
<RightSidebar
|
|
27
|
+
sessions={model.sessions}
|
|
28
|
+
activity={model.activity}
|
|
29
|
+
fsEntries={runtime.fsEntries}
|
|
30
|
+
fsPath={runtime.fsPath}
|
|
31
|
+
providerQuota={model.providerQuota}
|
|
32
|
+
running={runtime.running}
|
|
33
|
+
onOpenSession={runtime.openSession}
|
|
34
|
+
onCreateSession={runtime.createSession}
|
|
35
|
+
onBrowseRoots={runtime.browseRoots}
|
|
36
|
+
onBrowsePath={runtime.browsePath}
|
|
37
|
+
/>
|
|
38
|
+
<Composer
|
|
39
|
+
composer={model.composer}
|
|
40
|
+
running={runtime.running}
|
|
41
|
+
disabled={!model.activeSessionId}
|
|
42
|
+
onSubmit={runtime.submitPrompt}
|
|
43
|
+
onOpenLeft={() => setLeftOpen(true)}
|
|
44
|
+
onOpenRight={() => setRightOpen(true)}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { FormEvent } from "react";
|
|
3
|
+
import type { ComposerState } from "../model";
|
|
4
|
+
|
|
5
|
+
export type ComposerProps = {
|
|
6
|
+
composer: ComposerState;
|
|
7
|
+
running: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onSubmit: (prompt: string) => Promise<void>;
|
|
10
|
+
onOpenLeft: () => void;
|
|
11
|
+
onOpenRight: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function Composer({ composer, running, disabled = false, onSubmit, onOpenLeft, onOpenRight }: ComposerProps) {
|
|
15
|
+
const [prompt, setPrompt] = useState("");
|
|
16
|
+
const canSubmit = prompt.trim().length > 0 && !running && !disabled;
|
|
17
|
+
|
|
18
|
+
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
if (!canSubmit) return;
|
|
21
|
+
const nextPrompt = prompt.trim();
|
|
22
|
+
setPrompt("");
|
|
23
|
+
await onSubmit(nextPrompt);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<form className="composer" aria-label="Message composer" onSubmit={handleSubmit}>
|
|
28
|
+
<button type="button" className="mobile-toggle" onClick={onOpenLeft} aria-label="Open files">▦</button>
|
|
29
|
+
<div className="composer-box">
|
|
30
|
+
<textarea
|
|
31
|
+
rows={1}
|
|
32
|
+
value={prompt}
|
|
33
|
+
onChange={(event) => setPrompt(event.target.value)}
|
|
34
|
+
placeholder={composer.placeholder}
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
/>
|
|
37
|
+
<div className="composer-actions">
|
|
38
|
+
<button type="button" className="session-ring" aria-label="Session" />
|
|
39
|
+
<button type="button" className="chip-button">{running ? "Running" : composer.mode}</button>
|
|
40
|
+
<button type="button" className="icon-action" aria-label="Attach">+</button>
|
|
41
|
+
<button type="submit" className="send-icon" aria-label="Send" disabled={!canSubmit}>↑</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<button type="button" className="mobile-toggle" onClick={onOpenRight} aria-label="Open sessions">☰</button>
|
|
45
|
+
</form>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { FileTree as PierreFileTree, useFileTree } from "@pierre/trees/react";
|
|
2
|
+
import type { FileTreeRowDecorationContext } from "@pierre/trees";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { createProjectFileTreeInput } from "../fileTreeAdapter";
|
|
5
|
+
import type { FileNode } from "../model";
|
|
6
|
+
|
|
7
|
+
type FileExplorerProps = {
|
|
8
|
+
root: FileNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function FileExplorer({ root }: FileExplorerProps) {
|
|
12
|
+
const treeInput = useMemo(() => createProjectFileTreeInput(root), [root]);
|
|
13
|
+
const { model } = useFileTree({
|
|
14
|
+
density: "compact",
|
|
15
|
+
flattenEmptyDirectories: false,
|
|
16
|
+
gitStatus: treeInput.gitStatus,
|
|
17
|
+
icons: { set: "minimal", colored: false },
|
|
18
|
+
id: "march-project-tree",
|
|
19
|
+
initialExpandedPaths: treeInput.expandedPaths,
|
|
20
|
+
initialSelectedPaths: treeInput.selectedPaths,
|
|
21
|
+
paths: treeInput.paths,
|
|
22
|
+
renderRowDecoration: (context: FileTreeRowDecorationContext) => {
|
|
23
|
+
if (!treeInput.boundPaths.has(context.item.path)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return { text: "◆", title: "Bound to session" };
|
|
27
|
+
},
|
|
28
|
+
search: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<aside className="panel left-panel" aria-label="Projects">
|
|
33
|
+
<div className="projects-header">
|
|
34
|
+
<h3>Projects</h3>
|
|
35
|
+
<button className="menu-button" type="button" aria-label="Open project menu">
|
|
36
|
+
<span />
|
|
37
|
+
<span />
|
|
38
|
+
<span />
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
<div className="projects-body">
|
|
42
|
+
<PierreFileTree className="project-tree-host" model={model} aria-label="Project files" />
|
|
43
|
+
</div>
|
|
44
|
+
</aside>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { ActivityEvent, ProviderQuotaSnapshot, SessionSummary } from "../model";
|
|
3
|
+
import type { FsEntry } from "../runtime/client";
|
|
4
|
+
|
|
5
|
+
type RightSidebarProps = {
|
|
6
|
+
sessions: SessionSummary[];
|
|
7
|
+
activity: ActivityEvent[];
|
|
8
|
+
fsEntries: FsEntry[];
|
|
9
|
+
fsPath: string | null;
|
|
10
|
+
providerQuota?: ProviderQuotaSnapshot | null;
|
|
11
|
+
running: boolean;
|
|
12
|
+
onOpenSession: (sessionId: string) => Promise<void>;
|
|
13
|
+
onCreateSession: (workspacePath: string) => Promise<void>;
|
|
14
|
+
onBrowseRoots: () => Promise<void>;
|
|
15
|
+
onBrowsePath: (path: string) => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function RightSidebar(props: RightSidebarProps) {
|
|
19
|
+
const { sessions, activity, fsEntries, fsPath, providerQuota, running } = props;
|
|
20
|
+
const { onOpenSession, onCreateSession, onBrowseRoots, onBrowsePath } = props;
|
|
21
|
+
const [workspacePath, setWorkspacePath] = useState("");
|
|
22
|
+
const canCreate = workspacePath.trim().length > 0 && !running;
|
|
23
|
+
|
|
24
|
+
async function createFromPath(path = workspacePath) {
|
|
25
|
+
const nextPath = path.trim();
|
|
26
|
+
if (!nextPath || running) return;
|
|
27
|
+
await onCreateSession(nextPath);
|
|
28
|
+
setWorkspacePath("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<aside className="panel right-panel" aria-label="Sessions">
|
|
33
|
+
<div className="right-header">会话</div>
|
|
34
|
+
<div className="right-body">
|
|
35
|
+
<div className="workspace-picker" aria-label="Workspace picker">
|
|
36
|
+
<label htmlFor="workspace-path">Workspace</label>
|
|
37
|
+
<div className="workspace-input-row">
|
|
38
|
+
<input
|
|
39
|
+
id="workspace-path"
|
|
40
|
+
value={workspacePath}
|
|
41
|
+
onChange={(event) => setWorkspacePath(event.target.value)}
|
|
42
|
+
placeholder="Paste or browse a folder path"
|
|
43
|
+
/>
|
|
44
|
+
<button type="button" disabled={!canCreate} onClick={() => createFromPath()}>Open</button>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="workspace-path">{fsPath ?? "Roots"}</div>
|
|
47
|
+
<button type="button" className="fs-row" onClick={onBrowseRoots}>↖ Roots</button>
|
|
48
|
+
{fsEntries.map((entry) => (
|
|
49
|
+
<div key={entry.path} className="fs-entry-row">
|
|
50
|
+
<button type="button" onClick={() => onBrowsePath(entry.path)}>{entry.name}</button>
|
|
51
|
+
<button type="button" onClick={() => createFromPath(entry.path)}>Open</button>
|
|
52
|
+
</div>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{providerQuota ? <ProviderQuotaCard quota={providerQuota} /> : null}
|
|
57
|
+
|
|
58
|
+
<div className="right-divider">Sessions</div>
|
|
59
|
+
{sessions.map((session) => (
|
|
60
|
+
<button key={session.id} className={session.active ? "session-row active" : "session-row"} type="button" onClick={() => onOpenSession(session.id)}>
|
|
61
|
+
<span>{session.title}</span>
|
|
62
|
+
<time>{session.workspacePath ?? session.time}</time>
|
|
63
|
+
</button>
|
|
64
|
+
))}
|
|
65
|
+
<div className="right-divider">Activity</div>
|
|
66
|
+
{activity.map((event) => (
|
|
67
|
+
<button key={event.id} className="activity-row" type="button">
|
|
68
|
+
<span>{event.action}</span>
|
|
69
|
+
<time>{event.time}</time>
|
|
70
|
+
</button>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</aside>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ProviderQuotaCard({ quota }: { quota: ProviderQuotaSnapshot }) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="provider-quota" aria-label="Provider quota">
|
|
80
|
+
<div className="provider-quota-header">
|
|
81
|
+
<span>{quota.label}</span>
|
|
82
|
+
<time>{quota.providerId}</time>
|
|
83
|
+
</div>
|
|
84
|
+
{quota.limits.flatMap((limit) => limit.windows.map((window) => {
|
|
85
|
+
const left = Math.round(window.remainingPercent);
|
|
86
|
+
return (
|
|
87
|
+
<div key={`${limit.id}:${window.id}`} className="quota-row">
|
|
88
|
+
<div className="quota-row-main">
|
|
89
|
+
<span>{formatQuotaLabel(window.label)}</span>
|
|
90
|
+
<strong>{left}% left</strong>
|
|
91
|
+
</div>
|
|
92
|
+
<div className="quota-bar" aria-label={`${left}% quota left`}>
|
|
93
|
+
<span style={{ width: `${Math.max(0, Math.min(100, left))}%` }} />
|
|
94
|
+
</div>
|
|
95
|
+
<em>{formatReset(window.resetsAt)}</em>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}))}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatQuotaLabel(label: string) {
|
|
104
|
+
return label === "weekly" ? "Weekly limit:" : `${label} limit:`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatReset(resetsAt?: string | null) {
|
|
108
|
+
if (!resetsAt) return "reset unknown";
|
|
109
|
+
const date = new Date(resetsAt);
|
|
110
|
+
if (Number.isNaN(date.getTime())) return "reset unknown";
|
|
111
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
112
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
113
|
+
const month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getMonth()];
|
|
114
|
+
return `resets ${hours}:${minutes} on ${date.getDate()} ${month}`;
|
|
115
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { normalizeTimelineEvents } from "../timelineAdapter";
|
|
2
|
+
import type { WebUiModel } from "../model";
|
|
3
|
+
import { TimelineList } from "./timeline/TimelineList";
|
|
4
|
+
|
|
5
|
+
export type SessionTimelineProps = {
|
|
6
|
+
timeline: WebUiModel["timeline"];
|
|
7
|
+
connected: boolean;
|
|
8
|
+
error: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function SessionTimeline({ timeline, connected, error }: SessionTimelineProps) {
|
|
12
|
+
const items = normalizeTimelineEvents(timeline.events);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<main className="timeline" aria-label="Agent timeline">
|
|
16
|
+
<div className="main-header">
|
|
17
|
+
<span>Session</span>
|
|
18
|
+
<span className={connected ? "runtime-pill connected" : "runtime-pill"}>
|
|
19
|
+
{connected ? "runner" : "mock"}
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="timeline-scroll">
|
|
23
|
+
<div className="session-title">
|
|
24
|
+
<h1>{timeline.title}</h1>
|
|
25
|
+
<span>{error ?? timeline.meta}</span>
|
|
26
|
+
</div>
|
|
27
|
+
<TimelineList items={items} />
|
|
28
|
+
</div>
|
|
29
|
+
</main>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { TimelineItem } from "../../model";
|
|
2
|
+
|
|
3
|
+
type TimelineBlocksProps = {
|
|
4
|
+
item: TimelineItem;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function TimelineBlocks({ item }: TimelineBlocksProps) {
|
|
8
|
+
if (item.kind === "message") return <MessageBlock item={item} />;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<article className="message-row assistant-turn">
|
|
12
|
+
<div className="agent-dot march">M</div>
|
|
13
|
+
<div className="message-body"><AuxBlock item={item} /></div>
|
|
14
|
+
</article>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function MessageBlock({ item }: { item: Extract<TimelineItem, { kind: "message" }> }) {
|
|
19
|
+
return (
|
|
20
|
+
<article className={`message-row ${item.actor === "user" ? "user-turn" : "assistant-turn"}`}>
|
|
21
|
+
<div className={item.actor === "user" ? "agent-dot" : "agent-dot march"}>
|
|
22
|
+
{item.actor === "user" ? "U" : "M"}
|
|
23
|
+
</div>
|
|
24
|
+
<div className="message-body">
|
|
25
|
+
<p>{item.text}</p>
|
|
26
|
+
{item.time ? <time>{item.time}</time> : null}
|
|
27
|
+
</div>
|
|
28
|
+
</article>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function AuxBlock({ item }: { item: Exclude<TimelineItem, { kind: "message" }> }) {
|
|
33
|
+
switch (item.kind) {
|
|
34
|
+
case "thought":
|
|
35
|
+
return <ThoughtBlock item={item} />;
|
|
36
|
+
case "tool":
|
|
37
|
+
return <ToolBlock item={item} />;
|
|
38
|
+
case "diff":
|
|
39
|
+
return <DiffBlock item={item} />;
|
|
40
|
+
case "terminal":
|
|
41
|
+
return <TerminalBlock item={item} />;
|
|
42
|
+
case "permission":
|
|
43
|
+
return <PermissionBlock item={item} />;
|
|
44
|
+
case "error":
|
|
45
|
+
return <ErrorBlock item={item} />;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ThoughtBlock({ item }: { item: Extract<TimelineItem, { kind: "thought" }> }) {
|
|
50
|
+
return (
|
|
51
|
+
<details className="timeline-aux thought-block" open={item.status === "open"}>
|
|
52
|
+
<summary><span>thinking</span><strong>{item.title}</strong></summary>
|
|
53
|
+
<p>{item.text}</p>
|
|
54
|
+
</details>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ToolBlock({ item }: { item: Extract<TimelineItem, { kind: "tool" }> }) {
|
|
59
|
+
return (
|
|
60
|
+
<details className="timeline-aux tool-block" open={item.status !== "done"}>
|
|
61
|
+
<summary>
|
|
62
|
+
<span>{item.tool}</span>
|
|
63
|
+
<strong>{item.target}</strong>
|
|
64
|
+
<em>{item.status}</em>
|
|
65
|
+
</summary>
|
|
66
|
+
{item.summary ? <p>{item.summary}</p> : null}
|
|
67
|
+
</details>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function DiffBlock({ item }: { item: Extract<TimelineItem, { kind: "diff" }> }) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="timeline-aux diff-block">
|
|
74
|
+
<div className="aux-title"><span>diff</span><strong>{item.path}</strong></div>
|
|
75
|
+
<div className="diff-inline">
|
|
76
|
+
{item.lines.map((line) => (
|
|
77
|
+
<div key={`${line.kind}:${line.text}`} className={`diff-line ${line.kind}`}>{line.text}</div>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function TerminalBlock({ item }: { item: Extract<TimelineItem, { kind: "terminal" }> }) {
|
|
85
|
+
return (
|
|
86
|
+
<details className="timeline-aux terminal-block" open={item.status !== "done"}>
|
|
87
|
+
<summary><span>terminal</span><strong>{item.command}</strong><em>{item.status}</em></summary>
|
|
88
|
+
<pre>{item.output}</pre>
|
|
89
|
+
</details>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function PermissionBlock({ item }: { item: Extract<TimelineItem, { kind: "permission" }> }) {
|
|
94
|
+
return (
|
|
95
|
+
<div className={`timeline-aux permission-block ${item.status}`}>
|
|
96
|
+
<div className="aux-title"><span>permission</span><strong>{item.title}</strong><em>{item.status}</em></div>
|
|
97
|
+
<p>{item.detail}</p>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ErrorBlock({ item }: { item: Extract<TimelineItem, { kind: "error" }> }) {
|
|
103
|
+
return (
|
|
104
|
+
<div className="timeline-aux error-block">
|
|
105
|
+
<div className="aux-title"><span>error</span><strong>{item.message}</strong></div>
|
|
106
|
+
{item.detail ? <p>{item.detail}</p> : null}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TimelineItem } from "../../model";
|
|
2
|
+
import { TimelineBlocks } from "./TimelineBlocks";
|
|
3
|
+
|
|
4
|
+
export type TimelineListProps = {
|
|
5
|
+
items: TimelineItem[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function TimelineList({ items }: TimelineListProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="timeline-list" aria-label="Session events">
|
|
11
|
+
{items.map((item) => <TimelineBlocks key={item.id} item={item} />)}
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { GitStatusEntry } from "@pierre/trees";
|
|
2
|
+
import type { FileNode } from "./model";
|
|
3
|
+
|
|
4
|
+
export type ProjectFileTreeInput = {
|
|
5
|
+
boundPaths: Set<string>;
|
|
6
|
+
expandedPaths: string[];
|
|
7
|
+
gitStatus: GitStatusEntry[];
|
|
8
|
+
paths: string[];
|
|
9
|
+
selectedPaths: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function createProjectFileTreeInput(root: FileNode): ProjectFileTreeInput {
|
|
13
|
+
const input: ProjectFileTreeInput = {
|
|
14
|
+
boundPaths: new Set<string>(),
|
|
15
|
+
expandedPaths: [],
|
|
16
|
+
gitStatus: [],
|
|
17
|
+
paths: [],
|
|
18
|
+
selectedPaths: [],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
visitNode(root, "", input);
|
|
22
|
+
|
|
23
|
+
return input;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function visitNode(node: FileNode, parentPath: string, input: ProjectFileTreeInput) {
|
|
27
|
+
const path = joinPath(parentPath, node.name);
|
|
28
|
+
|
|
29
|
+
input.paths.push(node.kind === "folder" ? `${path}/` : path);
|
|
30
|
+
|
|
31
|
+
if (node.kind === "folder" && node.children?.length) {
|
|
32
|
+
input.expandedPaths.push(path);
|
|
33
|
+
}
|
|
34
|
+
if (node.selected || node.active) {
|
|
35
|
+
input.selectedPaths.push(path);
|
|
36
|
+
}
|
|
37
|
+
if (node.bound) {
|
|
38
|
+
input.boundPaths.add(path);
|
|
39
|
+
}
|
|
40
|
+
if (node.gitStatus) {
|
|
41
|
+
input.gitStatus.push({ path, status: node.gitStatus });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const child of node.children ?? []) {
|
|
45
|
+
visitNode(child, path, input);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function joinPath(parentPath: string, name: string) {
|
|
50
|
+
return parentPath ? `${parentPath}/${name}` : name;
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { App } from "./App";
|
|
4
|
+
import "./styles/tokens.css";
|
|
5
|
+
import "./styles/shell.css";
|
|
6
|
+
|
|
7
|
+
createRoot(document.getElementById("root")!).render(
|
|
8
|
+
<React.StrictMode>
|
|
9
|
+
<App />
|
|
10
|
+
</React.StrictMode>,
|
|
11
|
+
);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { WebUiModel } from "./model";
|
|
2
|
+
|
|
3
|
+
export const mockWebUiModel: WebUiModel = {
|
|
4
|
+
activeSessionId: "web-shell",
|
|
5
|
+
workspace: {
|
|
6
|
+
id: "root",
|
|
7
|
+
name: "march-cli-standalone",
|
|
8
|
+
kind: "folder",
|
|
9
|
+
selected: true,
|
|
10
|
+
bound: true,
|
|
11
|
+
children: [
|
|
12
|
+
{
|
|
13
|
+
id: "src",
|
|
14
|
+
name: "src",
|
|
15
|
+
kind: "folder",
|
|
16
|
+
children: [
|
|
17
|
+
{
|
|
18
|
+
id: "web-ui",
|
|
19
|
+
name: "web-ui",
|
|
20
|
+
kind: "folder",
|
|
21
|
+
children: [
|
|
22
|
+
{ id: "app", name: "App.tsx", kind: "file", active: true, bound: true, gitStatus: "modified" },
|
|
23
|
+
{ id: "model", name: "model.ts", kind: "file" },
|
|
24
|
+
{ id: "styles", name: "styles.css", kind: "file" },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{ id: "test", name: "test", kind: "folder" },
|
|
30
|
+
{ id: "agents", name: "AGENTS.md", kind: "file" },
|
|
31
|
+
{ id: "package", name: "package.json", kind: "file", gitStatus: "modified" },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
timeline: {
|
|
35
|
+
title: "Web shell",
|
|
36
|
+
meta: "mock adapter · fast",
|
|
37
|
+
events: [
|
|
38
|
+
{ id: "u1", type: "user_message", text: "参考 MindFS,做正式 Web UI。", time: "09:41" },
|
|
39
|
+
{ id: "m1", type: "assistant_message", text: "先组件化,runtime 保持不变。", time: "09:41" },
|
|
40
|
+
{
|
|
41
|
+
id: "th1",
|
|
42
|
+
type: "assistant_thought",
|
|
43
|
+
title: "Planning",
|
|
44
|
+
text: "把 runtime 事件先整理成 timeline item,再交给 UI 渲染。",
|
|
45
|
+
status: "closed",
|
|
46
|
+
},
|
|
47
|
+
{ id: "t1", type: "tool_call", tool: "read", target: "workspace", status: "running" },
|
|
48
|
+
{ id: "tr1", type: "tool_result", tool: "read", summary: "4 files inspected", status: "done" },
|
|
49
|
+
{
|
|
50
|
+
id: "d1",
|
|
51
|
+
type: "file_diff",
|
|
52
|
+
path: "src/web-ui/src/model.ts",
|
|
53
|
+
lines: [
|
|
54
|
+
{ kind: "add", text: "+ Timeline adapter model" },
|
|
55
|
+
{ kind: "keep", text: " runtime adapter pending" },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "term1",
|
|
60
|
+
type: "terminal_output",
|
|
61
|
+
command: "npm run test:fast",
|
|
62
|
+
output: "PASS web-ui.smoke.mjs",
|
|
63
|
+
status: "done",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "perm1",
|
|
67
|
+
type: "permission_request",
|
|
68
|
+
title: "Write files",
|
|
69
|
+
detail: "Edit local workspace source files",
|
|
70
|
+
status: "approved",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
sessions: [
|
|
75
|
+
{ id: "web-shell", title: "Web shell", time: "now", active: true },
|
|
76
|
+
{ id: "memory", title: "Memory", time: "1d" },
|
|
77
|
+
],
|
|
78
|
+
activity: [
|
|
79
|
+
{ id: "read", action: "read", time: "now" },
|
|
80
|
+
{ id: "edit", action: "edit", time: "2m" },
|
|
81
|
+
{ id: "test", action: "test", time: "5m" },
|
|
82
|
+
],
|
|
83
|
+
composer: {
|
|
84
|
+
mode: "Chat",
|
|
85
|
+
placeholder: "Message March…",
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type FileNode = {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
kind: "file" | "folder";
|
|
5
|
+
selected?: boolean;
|
|
6
|
+
active?: boolean;
|
|
7
|
+
bound?: boolean;
|
|
8
|
+
gitStatus?: "added" | "deleted" | "ignored" | "modified" | "renamed" | "untracked";
|
|
9
|
+
children?: FileNode[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type MarchTimelineEvent =
|
|
13
|
+
| { id: string; type: "user_message"; text: string; time?: string }
|
|
14
|
+
| { id: string; type: "assistant_message"; text: string; time?: string }
|
|
15
|
+
| { id: string; type: "assistant_thought"; title: string; text: string; status: "open" | "closed" }
|
|
16
|
+
| { id: string; type: "tool_call"; tool: string; target: string; status: "running" | "done" | "failed" }
|
|
17
|
+
| { id: string; type: "tool_result"; tool: string; summary: string; status: "done" | "failed" }
|
|
18
|
+
| { id: string; type: "file_diff"; path: string; lines: Array<{ kind: "add" | "remove" | "keep"; text: string }> }
|
|
19
|
+
| { id: string; type: "terminal_output"; command: string; output: string; status: "running" | "done" | "failed" }
|
|
20
|
+
| { id: string; type: "permission_request"; title: string; detail: string; status: "pending" | "approved" | "denied" }
|
|
21
|
+
| { id: string; type: "error"; message: string; detail?: string };
|
|
22
|
+
|
|
23
|
+
export type TimelineItem =
|
|
24
|
+
| { id: string; kind: "message"; actor: "user" | "march"; text: string; time?: string }
|
|
25
|
+
| { id: string; kind: "thought"; title: string; text: string; status: "open" | "closed" }
|
|
26
|
+
| { id: string; kind: "tool"; tool: string; target: string; status: "running" | "done" | "failed"; summary?: string }
|
|
27
|
+
| { id: string; kind: "diff"; path: string; lines: Array<{ kind: "add" | "remove" | "keep"; text: string }> }
|
|
28
|
+
| { id: string; kind: "terminal"; command: string; output: string; status: "running" | "done" | "failed" }
|
|
29
|
+
| { id: string; kind: "permission"; title: string; detail: string; status: "pending" | "approved" | "denied" }
|
|
30
|
+
| { id: string; kind: "error"; message: string; detail?: string };
|
|
31
|
+
|
|
32
|
+
export type SessionSummary = {
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
workspacePath?: string;
|
|
36
|
+
time: string;
|
|
37
|
+
active?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ActivityEvent = {
|
|
41
|
+
id: string;
|
|
42
|
+
action: string;
|
|
43
|
+
time: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type ProviderQuotaSnapshot = {
|
|
47
|
+
providerId: string;
|
|
48
|
+
modelId?: string | null;
|
|
49
|
+
label: string;
|
|
50
|
+
planType?: string | null;
|
|
51
|
+
capturedAt: string;
|
|
52
|
+
limits: Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
windows: Array<{
|
|
56
|
+
id: string;
|
|
57
|
+
label: string;
|
|
58
|
+
usedPercent: number;
|
|
59
|
+
remainingPercent: number;
|
|
60
|
+
resetsAt?: string | null;
|
|
61
|
+
}>;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type ComposerState = {
|
|
66
|
+
mode: string;
|
|
67
|
+
placeholder: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type WebUiModel = {
|
|
71
|
+
activeSessionId?: string | null;
|
|
72
|
+
workspace: FileNode;
|
|
73
|
+
timeline: {
|
|
74
|
+
title: string;
|
|
75
|
+
meta: string;
|
|
76
|
+
events: MarchTimelineEvent[];
|
|
77
|
+
};
|
|
78
|
+
sessions: SessionSummary[];
|
|
79
|
+
providerQuota?: ProviderQuotaSnapshot | null;
|
|
80
|
+
activity: ActivityEvent[];
|
|
81
|
+
composer: ComposerState;
|
|
82
|
+
};
|