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.
Files changed (72) hide show
  1. package/package.json +12 -1
  2. package/src/agent/code-search/cache.mjs +133 -0
  3. package/src/agent/code-search/chunk-rules.mjs +107 -0
  4. package/src/agent/code-search/chunker.mjs +125 -0
  5. package/src/agent/code-search/engine.mjs +109 -0
  6. package/src/agent/code-search/languages.mjs +25 -0
  7. package/src/agent/code-search/parser-pool.mjs +29 -0
  8. package/src/agent/code-search/rerank.mjs +43 -0
  9. package/src/agent/code-search/retrieval/bm25.mjs +47 -0
  10. package/src/agent/code-search/retrieval/fusion.mjs +18 -0
  11. package/src/agent/code-search/retrieval/model2vec.mjs +96 -0
  12. package/src/agent/code-search/retrieval/safetensors.mjs +49 -0
  13. package/src/agent/code-search/retrieval/vector.mjs +107 -0
  14. package/src/agent/code-search/retrieval/wordpiece.mjs +82 -0
  15. package/src/agent/code-search/scanner.mjs +84 -0
  16. package/src/agent/code-search/tokenize.mjs +16 -0
  17. package/src/agent/code-search/tool.mjs +75 -0
  18. package/src/agent/lifecycle/runner-lifecycle.mjs +16 -0
  19. package/src/agent/lifecycle/runtime-restart-tool.mjs +22 -0
  20. package/src/agent/runner/provider-quota-runtime.mjs +38 -0
  21. package/src/agent/runner.mjs +14 -14
  22. package/src/agent/runtime/remote-runner-client.mjs +9 -15
  23. package/src/agent/runtime/runner-ipc-target.mjs +10 -22
  24. package/src/agent/runtime/runner-process-client.mjs +101 -24
  25. package/src/agent/runtime/runner-runtime-host.mjs +2 -0
  26. package/src/agent/runtime/state/runner-state.mjs +81 -0
  27. package/src/agent/runtime/ui-event-bridge.mjs +2 -0
  28. package/src/agent/session/session-options.mjs +2 -1
  29. package/src/agent/tools.mjs +6 -1
  30. package/src/cli/args.mjs +14 -3
  31. package/src/cli/commands/catalog/visible-commands.mjs +5 -0
  32. package/src/cli/commands/help-command.mjs +1 -7
  33. package/src/cli/commands/registry/slash-command-registry.mjs +296 -0
  34. package/src/cli/commands/status-command.mjs +61 -35
  35. package/src/cli/input/autocomplete.mjs +2 -25
  36. package/src/cli/repl-loop.mjs +24 -41
  37. package/src/cli/slash-commands.mjs +19 -185
  38. package/src/cli/startup/app-runtime.mjs +201 -0
  39. package/src/cli/startup/configured-command.mjs +9 -0
  40. package/src/cli/startup/early-command.mjs +29 -0
  41. package/src/cli/turn/turn-input-preparer.mjs +41 -0
  42. package/src/context/system-core/base.md +5 -0
  43. package/src/main.mjs +47 -242
  44. package/src/provider/quota/codex.mjs +278 -0
  45. package/src/provider/quota/index.mjs +46 -0
  46. package/src/provider/quota/transport-observer.mjs +99 -0
  47. package/src/web-ui/command.mjs +112 -0
  48. package/src/web-ui/index.html +12 -0
  49. package/src/web-ui/runtime-host.mjs +188 -0
  50. package/src/web-ui/server.mjs +140 -0
  51. package/src/web-ui/session-manager.mjs +111 -0
  52. package/src/web-ui/src/App.tsx +7 -0
  53. package/src/web-ui/src/components/AppShell.tsx +48 -0
  54. package/src/web-ui/src/components/Composer.tsx +47 -0
  55. package/src/web-ui/src/components/FileExplorer.tsx +46 -0
  56. package/src/web-ui/src/components/RightSidebar.tsx +115 -0
  57. package/src/web-ui/src/components/SessionTimeline.tsx +31 -0
  58. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +109 -0
  59. package/src/web-ui/src/components/timeline/TimelineList.tsx +14 -0
  60. package/src/web-ui/src/fileTreeAdapter.ts +51 -0
  61. package/src/web-ui/src/main.tsx +11 -0
  62. package/src/web-ui/src/mockData.ts +87 -0
  63. package/src/web-ui/src/model.ts +82 -0
  64. package/src/web-ui/src/runtime/client.ts +81 -0
  65. package/src/web-ui/src/runtime/runtimeTimeline.ts +88 -0
  66. package/src/web-ui/src/runtime/useWebRuntime.ts +144 -0
  67. package/src/web-ui/src/styles/shell.css +166 -0
  68. package/src/web-ui/src/styles/tokens.css +116 -0
  69. package/src/web-ui/src/timelineAdapter.ts +43 -0
  70. package/src/web-ui/src/vite-env.d.ts +1 -0
  71. package/src/web-ui/tsconfig.json +20 -0
  72. package/src/web-ui/vite.config.mjs +11 -0
@@ -0,0 +1,81 @@
1
+ import type { ProviderQuotaSnapshot, SessionSummary, WebUiModel } from "../model";
2
+
3
+ export type RuntimeUiEvent =
4
+ | { type: "web_user_message"; text: string }
5
+ | { type: "turn_start" }
6
+ | { type: "turn_end" }
7
+ | { type: "assistant_reply_end" }
8
+ | { type: "text_delta"; delta: string }
9
+ | { type: "thinking_start" }
10
+ | { type: "thinking_delta"; delta: string }
11
+ | { type: "thinking_end"; tokens?: number }
12
+ | { type: "tool_start"; name: string; args?: unknown }
13
+ | { type: "tool_end"; name: string; isError?: boolean; result?: unknown }
14
+ | { type: "edit_diff"; path: string; diffLines?: Array<{ type?: string; text?: string }> }
15
+ | { type: "permission_request"; toolName: string; category?: string; params?: unknown }
16
+ | { type: "provider_quota_snapshot"; snapshot: ProviderQuotaSnapshot | null }
17
+ | { type: "status"; text: string }
18
+ | { type: "retry_start"; errorMessage?: string }
19
+ | { type: "retry_end"; success?: boolean; finalError?: string };
20
+
21
+ export type FsEntry = { name: string; path: string; kind: "root" | "directory" };
22
+
23
+ export async function fetchRuntimeSnapshot(sessionId?: string | null): Promise<WebUiModel> {
24
+ const response = await fetch(apiPath("/api/snapshot", { sessionId }));
25
+ if (!response.ok) throw new Error(await response.text());
26
+ return response.json();
27
+ }
28
+
29
+ export async function createRuntimeSession(workspacePath: string): Promise<{ session: SessionSummary; snapshot: WebUiModel }> {
30
+ const response = await fetch("/api/sessions", {
31
+ method: "POST",
32
+ headers: { "content-type": "application/json" },
33
+ body: JSON.stringify({ workspacePath }),
34
+ });
35
+ if (!response.ok) throw new Error(await response.text());
36
+ return response.json();
37
+ }
38
+
39
+ export async function fetchFsRoots(): Promise<FsEntry[]> {
40
+ const response = await fetch("/api/fs/roots");
41
+ if (!response.ok) throw new Error(await response.text());
42
+ return (await response.json()).roots;
43
+ }
44
+
45
+ export async function fetchFsList(path: string): Promise<FsEntry[]> {
46
+ const response = await fetch(apiPath("/api/fs/list", { path }));
47
+ if (!response.ok) throw new Error(await response.text());
48
+ return (await response.json()).entries;
49
+ }
50
+
51
+ export async function fetchProviderQuota(sessionId: string): Promise<ProviderQuotaSnapshot | null> {
52
+ const response = await fetch(apiPath("/api/provider-quota", { sessionId }));
53
+ if (!response.ok) throw new Error(await response.text());
54
+ return (await response.json()).snapshot ?? null;
55
+ }
56
+
57
+ export async function submitRuntimeTurn(sessionId: string, prompt: string) {
58
+ const response = await fetch("/api/turn", {
59
+ method: "POST",
60
+ headers: { "content-type": "application/json" },
61
+ body: JSON.stringify({ sessionId, prompt }),
62
+ });
63
+ if (!response.ok) throw new Error(await response.text());
64
+ return response.json();
65
+ }
66
+
67
+ export function connectRuntimeEvents(sessionId: string, onEvent: (event: RuntimeUiEvent) => void, onError: () => void) {
68
+ const source = new EventSource(apiPath("/api/events", { sessionId }));
69
+ source.addEventListener("runtime", (message) => {
70
+ onEvent(JSON.parse((message as MessageEvent).data) as RuntimeUiEvent);
71
+ });
72
+ source.onerror = onError;
73
+ return () => source.close();
74
+ }
75
+
76
+ function apiPath(path: string, params: Record<string, string | null | undefined>) {
77
+ const query = new URLSearchParams();
78
+ for (const [key, value] of Object.entries(params)) if (value) query.set(key, value);
79
+ const suffix = query.toString();
80
+ return suffix ? `${path}?${suffix}` : path;
81
+ }
@@ -0,0 +1,88 @@
1
+ import type { MarchTimelineEvent } from "../model";
2
+ import type { RuntimeUiEvent } from "./client";
3
+
4
+ export function applyRuntimeEvent(events: MarchTimelineEvent[], event: RuntimeUiEvent): MarchTimelineEvent[] {
5
+ const next = [...events];
6
+ const id = `${event.type}:${next.length}:${Date.now()}`;
7
+ switch (event.type) {
8
+ case "web_user_message":
9
+ next.push({ id, type: "user_message", text: event.text, time: nowTime() });
10
+ return next;
11
+ case "text_delta":
12
+ return appendAssistantDelta(next, event.delta, id);
13
+ case "thinking_start":
14
+ next.push({ id, type: "assistant_thought", title: "Thinking", text: "", status: "open" });
15
+ return next;
16
+ case "thinking_delta":
17
+ return appendThoughtDelta(next, event.delta, id);
18
+ case "thinking_end":
19
+ return closeThought(next);
20
+ case "tool_start":
21
+ next.push({ id, type: "tool_call", tool: event.name, target: formatArgs(event.args), status: "running" });
22
+ return next;
23
+ case "tool_end":
24
+ next.push({ id, type: "tool_result", tool: event.name, summary: formatResult(event.result), status: event.isError ? "failed" : "done" });
25
+ return next;
26
+ case "edit_diff":
27
+ next.push({ id, type: "file_diff", path: event.path, lines: toDiffLines(event.diffLines) });
28
+ return next;
29
+ case "permission_request":
30
+ next.push({ id, type: "permission_request", title: event.toolName, detail: event.category ?? "permission", status: "pending" });
31
+ return next;
32
+ case "status":
33
+ next.push({ id, type: "terminal_output", command: "status", output: event.text, status: "done" });
34
+ return next;
35
+ case "retry_start":
36
+ next.push({ id, type: "error", message: "Retrying", detail: event.errorMessage });
37
+ return next;
38
+ case "retry_end":
39
+ if (!event.success && event.finalError) next.push({ id, type: "error", message: "Retry failed", detail: event.finalError });
40
+ return next;
41
+ default:
42
+ return next;
43
+ }
44
+ }
45
+
46
+ function appendAssistantDelta(events: MarchTimelineEvent[], delta: string, id: string) {
47
+ const last = events.at(-1);
48
+ if (last?.type === "assistant_message") last.text += delta;
49
+ else events.push({ id, type: "assistant_message", text: delta, time: nowTime() });
50
+ return events;
51
+ }
52
+
53
+ function appendThoughtDelta(events: MarchTimelineEvent[], delta: string, id: string) {
54
+ const last = events.at(-1);
55
+ if (last?.type === "assistant_thought" && last.status === "open") last.text += delta;
56
+ else events.push({ id, type: "assistant_thought", title: "Thinking", text: delta, status: "open" });
57
+ return events;
58
+ }
59
+
60
+ function closeThought(events: MarchTimelineEvent[]) {
61
+ const last = events.at(-1);
62
+ if (last?.type === "assistant_thought") last.status = "closed";
63
+ return events;
64
+ }
65
+
66
+ function toDiffLines(lines: Array<{ type?: string; text?: string }> = []) {
67
+ return lines.map((line) => ({ kind: toDiffKind(line.type), text: line.text ?? "" }));
68
+ }
69
+
70
+ function toDiffKind(type?: string): "add" | "remove" | "keep" {
71
+ if (type === "add") return "add";
72
+ if (type === "del") return "remove";
73
+ return "keep";
74
+ }
75
+
76
+ function formatArgs(args: unknown) {
77
+ if (args === undefined || args === null) return "running";
78
+ return JSON.stringify(args).slice(0, 160);
79
+ }
80
+
81
+ function formatResult(result: unknown) {
82
+ if (result === undefined || result === null) return "done";
83
+ return typeof result === "string" ? result.slice(0, 240) : JSON.stringify(result).slice(0, 240);
84
+ }
85
+
86
+ function nowTime() {
87
+ return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit" }).format(new Date());
88
+ }
@@ -0,0 +1,144 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { mockWebUiModel } from "../mockData";
3
+ import type { MarchTimelineEvent, WebUiModel } from "../model";
4
+ import { applyRuntimeEvent } from "./runtimeTimeline";
5
+ import { connectRuntimeEvents, createRuntimeSession, fetchFsList, fetchFsRoots, fetchProviderQuota, fetchRuntimeSnapshot, submitRuntimeTurn } from "./client";
6
+ import type { FsEntry } from "./client";
7
+
8
+ export type WebRuntimeState = {
9
+ model: WebUiModel;
10
+ connected: boolean;
11
+ running: boolean;
12
+ error: string | null;
13
+ fsEntries: FsEntry[];
14
+ fsPath: string | null;
15
+ openSession: (sessionId: string) => Promise<void>;
16
+ createSession: (workspacePath: string) => Promise<void>;
17
+ browseRoots: () => Promise<void>;
18
+ browsePath: (path: string) => Promise<void>;
19
+ submitPrompt: (prompt: string) => Promise<void>;
20
+ };
21
+
22
+ export function useWebRuntime(): WebRuntimeState {
23
+ const [model, setModel] = useState<WebUiModel>(mockWebUiModel);
24
+ const [connected, setConnected] = useState(false);
25
+ const [running, setRunning] = useState(false);
26
+ const [error, setError] = useState<string | null>(null);
27
+ const [fsEntries, setFsEntries] = useState<FsEntry[]>([]);
28
+ const [fsPath, setFsPath] = useState<string | null>(null);
29
+ const activeSessionId = model.activeSessionId ?? null;
30
+ const timelineCache = useRef(new Map<string, MarchTimelineEvent[]>());
31
+
32
+ useEffect(() => {
33
+ let mounted = true;
34
+ fetchRuntimeSnapshot()
35
+ .then((snapshot) => {
36
+ if (!mounted) return;
37
+ setModel(snapshot);
38
+ setConnected(true);
39
+ })
40
+ .catch(() => setConnected(false));
41
+ fetchFsRoots().then((roots) => mounted && setFsEntries(roots)).catch(() => {});
42
+ return () => { mounted = false; };
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ if (!activeSessionId) return undefined;
47
+ const disconnect = connectRuntimeEvents(
48
+ activeSessionId,
49
+ (event) => {
50
+ setConnected(true);
51
+ if (event.type === "provider_quota_snapshot") {
52
+ setModel((current) => current.activeSessionId === activeSessionId
53
+ ? { ...current, providerQuota: event.snapshot }
54
+ : current);
55
+ return;
56
+ }
57
+ setModel((current) => {
58
+ const next = updateTimelineEvents(current, (events) => applyRuntimeEvent(events, event));
59
+ rememberTimeline(next, timelineCache.current);
60
+ return next;
61
+ });
62
+ },
63
+ () => setConnected(false),
64
+ );
65
+ refreshProviderQuota(activeSessionId).catch(() => undefined);
66
+ return disconnect;
67
+ }, [activeSessionId]);
68
+
69
+ async function openSession(sessionId: string) {
70
+ setError(null);
71
+ const snapshot = await fetchRuntimeSnapshot(sessionId);
72
+ setModel(restoreCachedTimeline(snapshot, timelineCache.current));
73
+ }
74
+
75
+ async function createSession(workspacePath: string) {
76
+ setRunning(true);
77
+ setError(null);
78
+ try {
79
+ const result = await createRuntimeSession(workspacePath);
80
+ setModel(result.snapshot);
81
+ rememberTimeline(result.snapshot, timelineCache.current);
82
+ } catch (err) {
83
+ setError(err instanceof Error ? err.message : String(err));
84
+ } finally {
85
+ setRunning(false);
86
+ }
87
+ }
88
+
89
+ async function refreshProviderQuota(sessionId: string) {
90
+ const providerQuota = await fetchProviderQuota(sessionId);
91
+ setModel((current) => current.activeSessionId === sessionId ? { ...current, providerQuota } : current);
92
+ }
93
+
94
+ async function browseRoots() {
95
+ setFsPath(null);
96
+ setFsEntries(await fetchFsRoots());
97
+ }
98
+
99
+ async function browsePath(path: string) {
100
+ setFsPath(path);
101
+ setFsEntries(await fetchFsList(path));
102
+ }
103
+
104
+ async function submitPrompt(prompt: string) {
105
+ if (!activeSessionId) {
106
+ setError("Choose a workspace before sending a message");
107
+ return;
108
+ }
109
+ setRunning(true);
110
+ setError(null);
111
+ try {
112
+ await submitRuntimeTurn(activeSessionId, prompt);
113
+ } catch (err) {
114
+ const message = err instanceof Error ? err.message : String(err);
115
+ setError(message);
116
+ setModel((current) => {
117
+ const next = updateTimelineEvents(current, (events) => [
118
+ ...events,
119
+ { id: `client-error:${Date.now()}`, type: "error", message },
120
+ ]);
121
+ rememberTimeline(next, timelineCache.current);
122
+ return next;
123
+ });
124
+ } finally {
125
+ setRunning(false);
126
+ }
127
+ }
128
+
129
+ return { model, connected, running, error, fsEntries, fsPath, openSession, createSession, browseRoots, browsePath, submitPrompt };
130
+ }
131
+
132
+ function restoreCachedTimeline(model: WebUiModel, cache: Map<string, MarchTimelineEvent[]>): WebUiModel {
133
+ const activeSessionId = model.activeSessionId;
134
+ const cached = activeSessionId ? cache.get(activeSessionId) : null;
135
+ return cached ? { ...model, timeline: { ...model.timeline, events: cached } } : model;
136
+ }
137
+
138
+ function rememberTimeline(model: WebUiModel, cache: Map<string, MarchTimelineEvent[]>) {
139
+ if (model.activeSessionId) cache.set(model.activeSessionId, model.timeline.events);
140
+ }
141
+
142
+ function updateTimelineEvents(model: WebUiModel, update: (events: MarchTimelineEvent[]) => MarchTimelineEvent[]): WebUiModel {
143
+ return { ...model, timeline: { ...model.timeline, events: update(model.timeline.events) } };
144
+ }
@@ -0,0 +1,166 @@
1
+ @layer components {
2
+ .app-shell {
3
+ height: 100dvh;
4
+ display: grid;
5
+ grid-template-columns: var(--shell-left-width) minmax(0, 1fr) var(--shell-right-width);
6
+ grid-template-rows: minmax(0, 1fr) auto;
7
+ grid-template-areas: "sidebar main right" "sidebar footer right";
8
+ background: var(--color-bg-page);
9
+ color: var(--color-text-primary);
10
+ overflow: hidden;
11
+ isolation: isolate;
12
+ }
13
+
14
+ .panel { min-height: 0; overflow: hidden; background: var(--color-bg-sidebar); }
15
+ .left-panel { grid-area: sidebar; border-right: 1px solid var(--color-border-subtle); }
16
+ .right-panel { grid-area: right; border-left: 1px solid var(--color-border-subtle); display: flex; flex-direction: column; }
17
+
18
+ .projects-header, .right-header, .main-header {
19
+ height: var(--header-height);
20
+ padding: 0 16px;
21
+ border-bottom: 1px solid var(--color-border-subtle);
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ background: var(--color-bg-sidebar);
26
+ color: var(--color-text-muted);
27
+ font-size: 11px;
28
+ font-weight: var(--font-weight-ui-medium);
29
+ text-transform: uppercase;
30
+ }
31
+ .projects-header h3 { margin: 0; font: inherit; letter-spacing: 0.5px; }
32
+ .menu-button {
33
+ width: 28px;
34
+ height: 28px;
35
+ border: 0;
36
+ border-radius: var(--radius-control);
37
+ background: transparent;
38
+ color: var(--color-text-muted);
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ gap: 3px;
43
+ }
44
+ .menu-button span { width: 3.6px; height: 3.6px; border-radius: 999px; background: currentColor; }
45
+ .menu-button:hover, .header-button:hover, .session-row:hover, .activity-row:hover { background: var(--color-hover); }
46
+ .projects-body { height: calc(100% - var(--header-height)); overflow: hidden; padding: var(--space-panel) 0; }
47
+ .project-tree-host {
48
+ height: 100%;
49
+ --trees-accent-override: var(--color-accent);
50
+ --trees-bg-override: var(--color-bg-sidebar);
51
+ --trees-bg-muted-override: var(--color-hover);
52
+ --trees-border-color-override: var(--color-border-subtle);
53
+ --trees-fg-override: var(--color-text-primary);
54
+ --trees-fg-muted-override: var(--color-text-muted);
55
+ --trees-selected-bg-override: var(--color-accent-soft);
56
+ --trees-selected-fg-override: var(--color-accent);
57
+ --trees-font-family-override: var(--font-sans);
58
+ --trees-font-size-override: 13px;
59
+ --trees-border-radius-override: var(--radius-row);
60
+ --trees-item-height: 30px;
61
+ --trees-item-padding-x-override: 8px;
62
+ --trees-item-margin-x-override: 8px;
63
+ --trees-item-row-gap-override: 4px;
64
+ --trees-level-gap-override: var(--tree-indent);
65
+ --trees-padding-inline-override: 0px;
66
+ --trees-scrollbar-gutter-override: 6px;
67
+ }
68
+
69
+ .timeline { grid-area: main; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--color-bg-surface); overflow: hidden; }
70
+ .main-header { background: var(--color-bg-surface); }
71
+ .runtime-pill { border-radius: 999px; padding: 2px 7px; color: var(--color-text-muted); background: var(--color-bg-subtle); font-size: 12px; }
72
+ .runtime-pill.connected { color: var(--color-accent); background: var(--color-accent-soft); }
73
+ .timeline-scroll { flex: 1; min-height: 0; overflow: auto; padding: 16px 18px 24px; }
74
+ .session-title { display: flex; align-items: baseline; gap: 8px; margin: 0 0 14px 40px; }
75
+ .session-title h1 { margin: 0; font-size: 17px; font-weight: var(--font-weight-ui-strong); letter-spacing: -0.01em; }
76
+ .session-title span { color: var(--color-text-muted); font-size: 12px; }
77
+ .timeline-list { max-width: 920px; margin: 0 auto; }
78
+ .message-row { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 10px; padding: 10px 0; }
79
+ .message-row + .message-row { border-top: 1px solid var(--color-border-subtle); }
80
+ .agent-dot { display: grid; place-items: center; width: 28px; height: 28px; border-radius: var(--radius-control); background: var(--palette-slate-700); color: var(--color-text-inverse); font-size: 12px; font-weight: 800; }
81
+ .agent-dot.march { background: var(--color-accent); }
82
+ .message-body { min-width: 0; padding-top: 3px; color: var(--color-text-primary); font-size: 14px; font-weight: var(--font-weight-content-normal); line-height: 1.55; }
83
+ .message-body p { margin: 0; }
84
+ .message-body time { display: block; margin-top: 4px; color: var(--color-text-muted); font-size: 11px; }
85
+ .timeline-aux { border: 1px solid var(--color-border-subtle); border-radius: var(--radius-control); padding: 7px 9px; background: var(--color-bg-subtle); }
86
+ .timeline-aux summary, .aux-title { display: flex; align-items: center; gap: 8px; min-height: 20px; }
87
+ .timeline-aux summary { cursor: pointer; }
88
+ .timeline-aux span, .timeline-aux em { color: var(--color-text-muted); font-size: 12px; font-style: normal; }
89
+ .timeline-aux strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: var(--font-weight-ui-medium); }
90
+ .timeline-aux p { margin: 7px 0 0; color: var(--color-text-muted); font-size: 13px; }
91
+ .tool-block span, .diff-inline, .terminal-block pre { font-family: var(--font-mono); }
92
+ .diff-inline { margin-top: 7px; border-left: 2px solid var(--color-border-strong); padding-left: 10px; font-size: 13px; }
93
+ .diff-line { padding: 2px 0; }
94
+ .diff-line.add { color: var(--color-success); }
95
+ .diff-line.remove, .error-block strong { color: var(--color-danger); }
96
+ .diff-line.keep { color: var(--color-text-muted); }
97
+ .terminal-block pre { margin: 7px 0 0; color: var(--color-text-muted); font-size: 12px; white-space: pre-wrap; }
98
+ .permission-block.pending strong { color: var(--color-warning); }
99
+ .permission-block.approved strong { color: var(--color-success); }
100
+
101
+ .right-body { flex: 1; min-height: 0; overflow: auto; padding: 12px; }
102
+ .workspace-picker { display: grid; gap: 7px; padding-bottom: 10px; }
103
+ .workspace-picker label { color: var(--color-text-muted); font-size: 11px; font-weight: var(--font-weight-ui-medium); text-transform: uppercase; }
104
+ .workspace-input-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px; }
105
+ .workspace-input-row input { min-width: 0; border: 1px solid var(--color-border-subtle); border-radius: var(--radius-control); padding: 7px 8px; background: var(--color-bg-surface); color: var(--color-text-primary); }
106
+ .workspace-input-row button, .fs-entry-row button, .fs-row { border: 0; border-radius: var(--radius-control); background: transparent; color: var(--color-text-muted); }
107
+ .workspace-input-row button { padding: 0 8px; background: var(--color-accent-soft); color: var(--color-accent); }
108
+ .workspace-input-row button:disabled { opacity: 0.45; }
109
+ .workspace-path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--color-text-muted); font-size: 12px; }
110
+ .fs-entry-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px; }
111
+ .fs-entry-row button, .fs-row { min-height: 28px; padding: 0 7px; text-align: left; }
112
+ .fs-entry-row button:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--color-text-primary); }
113
+ .provider-quota { margin-top: 12px; border-top: 1px solid var(--color-border-subtle); padding-top: 12px; display: grid; gap: 9px; }
114
+ .provider-quota-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; }
115
+ .provider-quota-header span { color: var(--color-text-primary); font-weight: var(--font-weight-ui-medium); }
116
+ .provider-quota-header time, .quota-row em { color: var(--color-text-muted); font-style: normal; }
117
+ .quota-row { display: grid; gap: 5px; font-size: 12px; }
118
+ .quota-row-main { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
119
+ .quota-row-main span { color: var(--color-text-primary); }
120
+ .quota-row-main strong { color: var(--color-accent); font-size: 12px; }
121
+ .quota-bar { height: 6px; overflow: hidden; border-radius: 999px; background: var(--color-bg-surface); box-shadow: inset 0 0 0 1px var(--color-border-subtle); }
122
+ .quota-bar span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 72%, white)); }
123
+ .session-row, .activity-row { width: 100%; min-height: 34px; border: 0; border-radius: var(--radius-control); padding: 7px 8px; background: transparent; color: var(--color-text-primary); display: flex; align-items: center; justify-content: space-between; gap: 8px; text-align: left; font-size: 13px; }
124
+ .session-row.active { background: var(--color-accent-soft); color: var(--color-accent); font-weight: var(--font-weight-ui-medium); }
125
+ .session-row time, .activity-row time { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--color-text-muted); font-size: 12px; }
126
+ .right-divider { margin: 12px 0 4px; padding-top: 12px; border-top: 1px solid var(--color-border-subtle); color: var(--color-text-muted); font-size: 11px; font-weight: var(--font-weight-ui-medium); text-transform: uppercase; }
127
+ .activity-row { color: var(--color-text-muted); }
128
+
129
+ .composer { grid-area: footer; min-width: 0; padding: 0 16px 12px; display: grid; grid-template-columns: 1fr; align-items: end; background: var(--color-bg-surface); }
130
+ .composer-box { width: 100%; min-height: var(--composer-min-height); border: 1px solid var(--color-border-strong); border-radius: var(--radius-composer); background: var(--color-bg-sidebar); display: flex; align-items: center; position: relative; overflow: visible; }
131
+ .composer-box:focus-within { border-color: var(--color-accent); box-shadow: 0 0 0 1px var(--color-accent-soft); }
132
+ .composer textarea { width: 100%; max-height: 160px; resize: none; border: 0; outline: 0; background: transparent; color: var(--color-text-primary); font-weight: var(--font-weight-content-normal); padding: 12px 124px 12px 12px; }
133
+ .composer-actions { position: absolute; right: 8px; bottom: 8px; display: flex; align-items: center; gap: 2px; }
134
+ .session-ring, .chip-button, .icon-action, .send-icon, .mobile-toggle { width: 28px; height: 28px; border: 0; border-radius: var(--radius-control); background: transparent; color: var(--color-text-muted); display: inline-flex; align-items: center; justify-content: center; }
135
+ .session-ring::before { content: ""; width: 14px; height: 14px; border: 2px solid var(--color-accent); border-radius: 999px; }
136
+ .chip-button { width: auto; padding: 0 8px; font-size: 12px; }
137
+ .icon-action:hover, .chip-button:hover, .mobile-toggle:hover { background: var(--color-hover); }
138
+ .send-icon { background: var(--color-accent); color: var(--color-text-inverse); font-size: 16px; font-weight: 700; }
139
+ .send-icon:disabled { opacity: 0.45; }
140
+ .mobile-toggle, .overlay { display: none; }
141
+
142
+ @media (max-width: 920px) {
143
+ html, body { position: fixed; inset: 0; width: 100%; min-height: 0; }
144
+ .app-shell { display: flex; flex-direction: column; height: 100dvh; padding-top: env(safe-area-inset-top, 0); }
145
+ .timeline { flex: 1; min-height: 0; }
146
+ .composer { flex: 0 0 auto; grid-template-columns: 30px minmax(0, 1fr) 30px; gap: 1px; padding: 0 1px max(2px, env(safe-area-inset-bottom)); }
147
+ .mobile-toggle { display: inline-flex; height: 44px; align-self: center; }
148
+ .composer-box { border-radius: 10px; min-height: 44px; }
149
+ .composer textarea { padding-right: 114px; }
150
+ .left-panel, .right-panel {
151
+ position: fixed;
152
+ top: env(safe-area-inset-top, 0);
153
+ bottom: 0;
154
+ z-index: 30;
155
+ width: var(--shell-mobile-panel-width);
156
+ transform: translateX(-100%);
157
+ transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
158
+ background: var(--color-bg-sidebar);
159
+ box-shadow: 4px 0 24px var(--color-shadow-drawer);
160
+ }
161
+ .left-panel { left: 0; border-top-right-radius: 14px; border-bottom-right-radius: 14px; }
162
+ .right-panel { right: 0; transform: translateX(100%); border-top-left-radius: 14px; border-bottom-left-radius: 14px; box-shadow: -4px 0 24px var(--color-shadow-drawer); }
163
+ .app-shell[data-left-open="true"] .left-panel, .app-shell[data-right-open="true"] .right-panel { transform: translateX(0); }
164
+ .app-shell[data-left-open="true"] .overlay, .app-shell[data-right-open="true"] .overlay { display: block; position: fixed; inset: 0; z-index: 20; background: var(--color-scrim); }
165
+ }
166
+ }
@@ -0,0 +1,116 @@
1
+ @layer theme.palette, theme.semantic, theme.component, base;
2
+
3
+ @layer theme.palette {
4
+ :root {
5
+ color-scheme: light dark;
6
+ --font-sans:
7
+ "Segoe UI Variable Text",
8
+ "Segoe UI Variable",
9
+ "Segoe UI",
10
+ -apple-system,
11
+ BlinkMacSystemFont,
12
+ Roboto,
13
+ "Helvetica Neue",
14
+ Arial,
15
+ DengXian,
16
+ "等线",
17
+ "Microsoft YaHei UI",
18
+ sans-serif;
19
+ --font-mono:
20
+ ui-monospace,
21
+ "SFMono-Regular",
22
+ "Cascadia Code",
23
+ "JetBrains Mono",
24
+ "Fira Code",
25
+ Consolas,
26
+ "Liberation Mono",
27
+ Menlo,
28
+ monospace;
29
+ --font-weight-ui-normal: 500;
30
+ --font-weight-content-normal: 600;
31
+ --font-weight-ui-medium: 600;
32
+ --font-weight-ui-strong: 700;
33
+ --palette-white: #ffffff;
34
+ --palette-slate-50: #f8fafc;
35
+ --palette-slate-100: #f1f5f9;
36
+ --palette-slate-200: #e2e8f0;
37
+ --palette-slate-400: #94a3b8;
38
+ --palette-slate-500: #64748b;
39
+ --palette-slate-700: #334155;
40
+ --palette-slate-900: #0f172a;
41
+ --palette-slate-950: #020617;
42
+ --palette-blue-400: #60a5fa;
43
+ --palette-blue-600: #2563eb;
44
+ --palette-green-700: #15803d;
45
+ --palette-amber-600: #d97706;
46
+ --palette-red-600: #dc2626;
47
+ }
48
+ }
49
+
50
+ @layer theme.semantic {
51
+ :root {
52
+ --color-bg-page: #f3f4f6;
53
+ --color-bg-surface: var(--palette-white);
54
+ --color-bg-sidebar: rgba(255, 255, 255, 0.74);
55
+ --color-bg-subtle: var(--palette-slate-50);
56
+ --color-text-primary: var(--palette-slate-900);
57
+ --color-text-muted: var(--palette-slate-500);
58
+ --color-text-inverse: var(--palette-white);
59
+ --color-border-subtle: rgba(15, 23, 42, 0.08);
60
+ --color-border-strong: rgba(15, 23, 42, 0.14);
61
+ --color-accent: var(--palette-blue-600);
62
+ --color-accent-soft: rgba(37, 99, 235, 0.08);
63
+ --color-success: var(--palette-green-700);
64
+ --color-warning: var(--palette-amber-600);
65
+ --color-danger: var(--palette-red-600);
66
+ --color-hover: rgba(0, 0, 0, 0.05);
67
+ --color-shadow-drawer: rgba(0, 0, 0, 0.15);
68
+ --color-scrim: rgba(0, 0, 0, 0.3);
69
+ }
70
+
71
+ @media (prefers-color-scheme: dark) {
72
+ :root {
73
+ --color-bg-page: var(--palette-slate-900);
74
+ --color-bg-surface: var(--palette-slate-950);
75
+ --color-bg-sidebar: var(--palette-slate-900);
76
+ --color-bg-subtle: var(--palette-slate-900);
77
+ --color-text-primary: var(--palette-slate-50);
78
+ --color-text-muted: var(--palette-slate-400);
79
+ --color-border-subtle: rgba(148, 163, 184, 0.12);
80
+ --color-border-strong: rgba(148, 163, 184, 0.2);
81
+ --color-accent: var(--palette-blue-400);
82
+ --color-accent-soft: rgba(96, 165, 250, 0.14);
83
+ --color-hover: rgba(148, 163, 184, 0.08);
84
+ }
85
+ }
86
+ }
87
+
88
+ @layer theme.component {
89
+ :root {
90
+ --shell-left-width: 260px;
91
+ --shell-right-width: 280px;
92
+ --shell-mobile-panel-width: 75vw;
93
+ --header-height: 36px;
94
+ --composer-min-height: 46px;
95
+ --radius-row: 6px;
96
+ --radius-control: 8px;
97
+ --radius-composer: 12px;
98
+ --space-panel: 8px;
99
+ --tree-indent: 16px;
100
+ }
101
+ }
102
+
103
+ @layer base {
104
+ * { box-sizing: border-box; }
105
+ html, body, #root { height: 100%; margin: 0; overflow: hidden; }
106
+ body {
107
+ background: var(--color-bg-page);
108
+ color: var(--color-text-primary);
109
+ font-family: var(--font-sans);
110
+ font-weight: var(--font-weight-ui-normal);
111
+ -webkit-font-smoothing: antialiased;
112
+ text-rendering: optimizeLegibility;
113
+ }
114
+ button, textarea { font: inherit; }
115
+ button { cursor: pointer; }
116
+ }
@@ -0,0 +1,43 @@
1
+ import type { MarchTimelineEvent, TimelineItem } from "./model";
2
+
3
+ export function normalizeTimelineEvents(events: MarchTimelineEvent[]): TimelineItem[] {
4
+ const items: TimelineItem[] = [];
5
+
6
+ for (const event of events) {
7
+ if (event.type === "tool_result") {
8
+ const previous = items.at(-1);
9
+ if (previous?.kind === "tool" && previous.tool === event.tool) {
10
+ previous.summary = event.summary;
11
+ previous.status = event.status;
12
+ continue;
13
+ }
14
+ }
15
+
16
+ items.push(toTimelineItem(event));
17
+ }
18
+
19
+ return items;
20
+ }
21
+
22
+ function toTimelineItem(event: MarchTimelineEvent): TimelineItem {
23
+ switch (event.type) {
24
+ case "user_message":
25
+ return { id: event.id, kind: "message", actor: "user", text: event.text, time: event.time };
26
+ case "assistant_message":
27
+ return { id: event.id, kind: "message", actor: "march", text: event.text, time: event.time };
28
+ case "assistant_thought":
29
+ return { id: event.id, kind: "thought", title: event.title, text: event.text, status: event.status };
30
+ case "tool_call":
31
+ return { id: event.id, kind: "tool", tool: event.tool, target: event.target, status: event.status };
32
+ case "tool_result":
33
+ return { id: event.id, kind: "tool", tool: event.tool, target: "result", status: event.status, summary: event.summary };
34
+ case "file_diff":
35
+ return { id: event.id, kind: "diff", path: event.path, lines: event.lines };
36
+ case "terminal_output":
37
+ return { id: event.id, kind: "terminal", command: event.command, output: event.output, status: event.status };
38
+ case "permission_request":
39
+ return { id: event.id, kind: "permission", title: event.title, detail: event.detail, status: event.status };
40
+ case "error":
41
+ return { id: event.id, kind: "error", message: event.message, detail: event.detail };
42
+ }
43
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Bundler",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src"]
20
+ }
@@ -0,0 +1,11 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import { defineConfig } from "vite";
3
+
4
+ export default defineConfig({
5
+ root: "src/web-ui",
6
+ plugins: [react()],
7
+ server: {
8
+ host: "127.0.0.1",
9
+ port: 4174,
10
+ },
11
+ });