march-cli 0.1.0
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/bin/march.mjs +13 -0
- package/package.json +36 -0
- package/src/agent/command-exec-tool.mjs +91 -0
- package/src/agent/context-stats-tool.mjs +57 -0
- package/src/agent/editing/diff-apply.mjs +28 -0
- package/src/agent/editing/diff-format.mjs +57 -0
- package/src/agent/file-edit-tool.mjs +276 -0
- package/src/agent/find-tool.mjs +112 -0
- package/src/agent/model-payload-dumper.mjs +201 -0
- package/src/agent/pi-session/pi-session-sidecar-failure.mjs +10 -0
- package/src/agent/provider/payload-messages.mjs +138 -0
- package/src/agent/read-file-tool.mjs +112 -0
- package/src/agent/runner/fast-model.mjs +36 -0
- package/src/agent/runner/runner-cleanup.mjs +12 -0
- package/src/agent/runner/runner-init.mjs +15 -0
- package/src/agent/runner/runner-session-state.mjs +40 -0
- package/src/agent/runner.mjs +266 -0
- package/src/agent/runtime/runner-runtime-host.mjs +73 -0
- package/src/agent/runtime/runtime-factory.mjs +42 -0
- package/src/agent/runtime/runtime-host.mjs +34 -0
- package/src/agent/session/session-auto-name.mjs +41 -0
- package/src/agent/session/session-binding.mjs +12 -0
- package/src/agent/session/session-options.mjs +46 -0
- package/src/agent/tool-names.mjs +1 -0
- package/src/agent/tool-result.mjs +3 -0
- package/src/agent/tools.mjs +54 -0
- package/src/agent/turn/turn-events.mjs +64 -0
- package/src/agent/turn/turn-runner.mjs +103 -0
- package/src/auth/login-command.mjs +90 -0
- package/src/auth/storage.mjs +33 -0
- package/src/cli/args.mjs +71 -0
- package/src/cli/commands/copy-command.mjs +73 -0
- package/src/cli/commands/export-command.mjs +206 -0
- package/src/cli/commands/extensions-command.mjs +53 -0
- package/src/cli/commands/help-command.mjs +7 -0
- package/src/cli/commands/model-command.mjs +110 -0
- package/src/cli/commands/paste-image-command.mjs +43 -0
- package/src/cli/commands/provider-command.mjs +55 -0
- package/src/cli/commands/status-command.mjs +157 -0
- package/src/cli/commands/thinking-command.mjs +80 -0
- package/src/cli/fallback-ui.mjs +156 -0
- package/src/cli/input/attachment-tokens.mjs +20 -0
- package/src/cli/input/autocomplete.mjs +106 -0
- package/src/cli/input/external-editor.mjs +39 -0
- package/src/cli/input/history-store.mjs +35 -0
- package/src/cli/input/image-clipboard.mjs +55 -0
- package/src/cli/input/keybinding-dispatch.mjs +76 -0
- package/src/cli/input/keybindings.mjs +96 -0
- package/src/cli/input/mode-state.mjs +43 -0
- package/src/cli/input/prompt-templates.mjs +84 -0
- package/src/cli/input/select-with-keyboard.mjs +67 -0
- package/src/cli/permissions.mjs +103 -0
- package/src/cli/repl-commands.mjs +86 -0
- package/src/cli/repl-loop.mjs +157 -0
- package/src/cli/selector-list.mjs +21 -0
- package/src/cli/session/pi-session-switch-command.mjs +41 -0
- package/src/cli/session/session-command.mjs +23 -0
- package/src/cli/session/session-list-command.mjs +68 -0
- package/src/cli/session/session-name-command.mjs +26 -0
- package/src/cli/session/session-source-command.mjs +89 -0
- package/src/cli/session/session-switch-command.mjs +1 -0
- package/src/cli/shell/shell-command.mjs +55 -0
- package/src/cli/shell/shell-drawer-controls.mjs +33 -0
- package/src/cli/shell/shell-drawer.mjs +192 -0
- package/src/cli/shell/shell-split-layout.mjs +70 -0
- package/src/cli/slash-commands.mjs +176 -0
- package/src/cli/startup/startup-banner.mjs +17 -0
- package/src/cli/startup/startup-session.mjs +51 -0
- package/src/cli/status-line-updater.mjs +74 -0
- package/src/cli/tool-output.mjs +9 -0
- package/src/cli/tui/editor/external-editor-runner.mjs +24 -0
- package/src/cli/tui/input/mouse-selection-controller.mjs +89 -0
- package/src/cli/tui/input/mouse-tracking.mjs +20 -0
- package/src/cli/tui/layout/main-pane-layout.mjs +38 -0
- package/src/cli/tui/layout/safe-render-boundary.mjs +46 -0
- package/src/cli/tui/markdown-renderer.mjs +279 -0
- package/src/cli/tui/output/scroll-state.mjs +79 -0
- package/src/cli/tui/output/tool-card-renderer.mjs +59 -0
- package/src/cli/tui/output-buffer.mjs +297 -0
- package/src/cli/tui/permission-request-ui.mjs +18 -0
- package/src/cli/tui/recall-rendering.mjs +25 -0
- package/src/cli/tui/select/editor-select-list.mjs +111 -0
- package/src/cli/tui/selection-screen.mjs +212 -0
- package/src/cli/tui/status/retry-status.mjs +72 -0
- package/src/cli/tui/status/spinner-status.mjs +42 -0
- package/src/cli/tui/status/status-bar.mjs +88 -0
- package/src/cli/tui/syntax/highlighting.mjs +277 -0
- package/src/cli/tui/syntax/languages.mjs +91 -0
- package/src/cli/tui/syntax/tree-sitter/bash.highlights.scm +261 -0
- package/src/cli/tui/syntax/tree-sitter/c.highlights.scm +341 -0
- package/src/cli/tui/syntax/tree-sitter/cpp.highlights.scm +268 -0
- package/src/cli/tui/syntax/tree-sitter/csharp.highlights.scm +577 -0
- package/src/cli/tui/syntax/tree-sitter/css.highlights.scm +109 -0
- package/src/cli/tui/syntax/tree-sitter/diff.highlights.scm +49 -0
- package/src/cli/tui/syntax/tree-sitter/go.highlights.scm +254 -0
- package/src/cli/tui/syntax/tree-sitter/html.highlights.scm +13 -0
- package/src/cli/tui/syntax/tree-sitter/java.highlights.scm +330 -0
- package/src/cli/tui/syntax/tree-sitter/json.highlights.scm +38 -0
- package/src/cli/tui/syntax/tree-sitter/php.highlights.scm +203 -0
- package/src/cli/tui/syntax/tree-sitter/python.highlights.scm +137 -0
- package/src/cli/tui/syntax/tree-sitter/ruby.highlights.scm +309 -0
- package/src/cli/tui/syntax/tree-sitter/rust.highlights.scm +531 -0
- package/src/cli/tui/syntax/tree-sitter/toml.highlights.scm +39 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-bash.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-c-sharp.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-c.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-cpp.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-css.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-diff.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-go.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-html.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-java.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-json.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-php.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-python.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-ruby.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-rust.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-toml.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-yaml.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tsx.highlights.scm +35 -0
- package/src/cli/tui/syntax/tree-sitter/typescript.highlights.scm +35 -0
- package/src/cli/tui/syntax/tree-sitter/yaml.highlights.scm +99 -0
- package/src/cli/tui/tool-rendering.mjs +194 -0
- package/src/cli/tui/tui-diff-rendering.mjs +157 -0
- package/src/cli/tui/tui-handlers.mjs +110 -0
- package/src/cli/tui/tui-input-controller.mjs +61 -0
- package/src/cli/tui/ui-theme.mjs +148 -0
- package/src/cli/ui.mjs +299 -0
- package/src/config/config-json.mjs +73 -0
- package/src/config/dotenv.mjs +20 -0
- package/src/config/features.mjs +75 -0
- package/src/config/loader.mjs +109 -0
- package/src/config/settings-command.mjs +97 -0
- package/src/context/diagnostics.mjs +70 -0
- package/src/context/engine.mjs +148 -0
- package/src/context/injections.mjs +26 -0
- package/src/context/project-context.mjs +20 -0
- package/src/context/session-status.mjs +15 -0
- package/src/context/shell-layers.mjs +23 -0
- package/src/context/system-core/base.md +60 -0
- package/src/context/system-core/prompts/deepseek-v4-pro.md +3 -0
- package/src/context/system-core/prompts/default.md +3 -0
- package/src/context/system-core.mjs +35 -0
- package/src/debug/model-context-dumper.mjs +52 -0
- package/src/extensions/discovery.mjs +40 -0
- package/src/extensions/lifecycle-adapter.mjs +210 -0
- package/src/extensions/lifecycle-manifest.mjs +69 -0
- package/src/image-gen/index.mjs +7 -0
- package/src/image-gen/provider.mjs +231 -0
- package/src/image-gen/tool.mjs +84 -0
- package/src/lsp/client.mjs +204 -0
- package/src/lsp/diagnostic-store.mjs +39 -0
- package/src/lsp/servers.mjs +212 -0
- package/src/lsp/service.mjs +65 -0
- package/src/main.mjs +294 -0
- package/src/mcp/client.mjs +195 -0
- package/src/mcp/config.mjs +130 -0
- package/src/mcp/index.mjs +48 -0
- package/src/mcp/tools.mjs +98 -0
- package/src/memory/database.mjs +219 -0
- package/src/memory/glossary.mjs +124 -0
- package/src/memory/graph/graph-cascades.mjs +109 -0
- package/src/memory/graph/graph-diagnostics.mjs +73 -0
- package/src/memory/graph/graph-path-removal.mjs +50 -0
- package/src/memory/graph/graph-path-utils.mjs +17 -0
- package/src/memory/graph/graph-primitives.mjs +103 -0
- package/src/memory/graph/graph-read.mjs +159 -0
- package/src/memory/graph.mjs +282 -0
- package/src/memory/markdown/markdown-delete.mjs +23 -0
- package/src/memory/markdown/markdown-format.mjs +128 -0
- package/src/memory/markdown/markdown-recall.mjs +28 -0
- package/src/memory/markdown/ripgrep.mjs +16 -0
- package/src/memory/markdown/sqlite-index.mjs +87 -0
- package/src/memory/markdown-store.mjs +286 -0
- package/src/memory/markdown-tools.mjs +103 -0
- package/src/memory/search.mjs +142 -0
- package/src/memory/snapshot.mjs +86 -0
- package/src/memory/system-views.mjs +120 -0
- package/src/memory/tools.mjs +282 -0
- package/src/notification/desktop-notifier.mjs +85 -0
- package/src/platform/open-file.mjs +28 -0
- package/src/provider/config-command.mjs +129 -0
- package/src/provider/presets.mjs +72 -0
- package/src/session/attachment-display.mjs +16 -0
- package/src/session/attachment-references.mjs +65 -0
- package/src/session/attachments.mjs +140 -0
- package/src/session/persist.mjs +1 -0
- package/src/session/pi-manager.mjs +34 -0
- package/src/session/session-utils.mjs +16 -0
- package/src/session/sidecar-sync.mjs +19 -0
- package/src/session/sidecar.mjs +68 -0
- package/src/session/transcript.mjs +83 -0
- package/src/session/tree.mjs +42 -0
- package/src/shell/cli-runtime.mjs +11 -0
- package/src/shell/hints.mjs +12 -0
- package/src/shell/node-pty-adapter.mjs +81 -0
- package/src/shell/runtime-state.mjs +126 -0
- package/src/shell/runtime.mjs +244 -0
- package/src/shell/screen-buffer.mjs +136 -0
- package/src/shell/tool-read.mjs +74 -0
- package/src/shell/tools.mjs +299 -0
- package/src/supergrok/actions/image-generate.mjs +60 -0
- package/src/supergrok/actions/search.mjs +78 -0
- package/src/supergrok/auth.mjs +36 -0
- package/src/supergrok/constants.mjs +18 -0
- package/src/supergrok/oauth-provider.mjs +278 -0
- package/src/supergrok/provider.mjs +36 -0
- package/src/supergrok/response.mjs +76 -0
- package/src/supergrok/tool.mjs +61 -0
- package/src/text/ansi.mjs +3 -0
- package/src/web/config-command.mjs +43 -0
- package/src/web/fetch.mjs +78 -0
- package/src/web/presets.mjs +16 -0
- package/src/web/search.mjs +83 -0
- package/src/web/tools.mjs +107 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { getProviderLabel } from "../../provider/presets.mjs";
|
|
2
|
+
import { globalConfigJsonPath, upsertModelSelection } from "../../config/config-json.mjs";
|
|
3
|
+
|
|
4
|
+
export function parseModelCommand(input) {
|
|
5
|
+
if (input !== "/model" && !input.startsWith("/model ")) {
|
|
6
|
+
return { type: "none" };
|
|
7
|
+
}
|
|
8
|
+
const arg = input.slice("/model".length).trim();
|
|
9
|
+
if (!arg) return { type: "select-interactive" };
|
|
10
|
+
return { type: "error", message: "Use /model without arguments or Ctrl+L to choose a model." };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function selectModelByIndex(index, { runner }) {
|
|
14
|
+
const scopedModels = runner.getScopedModels?.() || [];
|
|
15
|
+
if (scopedModels.length === 0) return "(no available models - run `march provider --config`)";
|
|
16
|
+
const selected = scopedModels[index - 1];
|
|
17
|
+
if (!selected) return `Error: model index out of range: ${index}`;
|
|
18
|
+
await runner.setModel(selected.model);
|
|
19
|
+
const name = selected.model.name || selected.model.id;
|
|
20
|
+
return `Model: ${name} (${selected.model.provider})`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildModelSelectItems({ current, scopedModels = [] }) {
|
|
24
|
+
return scopedModels.map(({ model }, index) => ({
|
|
25
|
+
value: String(index),
|
|
26
|
+
label: `${getProviderLabel(model.provider)} / ${model.name || model.id}`,
|
|
27
|
+
description: current && model.id === current.id && model.provider === current.provider ? "current" : model.provider,
|
|
28
|
+
model,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function handleModelCommand(parsed, { runner, ui = null, configHomeDir } = {}) {
|
|
33
|
+
if (parsed.type === "select-interactive") {
|
|
34
|
+
const scopedModels = runner.getScopedModels?.() || [];
|
|
35
|
+
if (!ui?.selectList || scopedModels.length === 0) return "Use Ctrl+L to choose a model.";
|
|
36
|
+
const current = runner.getCurrentModel?.();
|
|
37
|
+
const selectedIndex = Math.max(0, scopedModels.findIndex(({ model }) =>
|
|
38
|
+
current && model.id === current.id && model.provider === current.provider
|
|
39
|
+
));
|
|
40
|
+
const item = await ui.selectList({
|
|
41
|
+
items: buildModelSelectItems({ current, scopedModels }),
|
|
42
|
+
selectedIndex,
|
|
43
|
+
width: 72,
|
|
44
|
+
suppressInitialConfirm: true,
|
|
45
|
+
searchable: true,
|
|
46
|
+
getSearchText: modelSelectSearchText,
|
|
47
|
+
});
|
|
48
|
+
if (!item) return "Model unchanged.";
|
|
49
|
+
const model = await runner.setModel(item.model);
|
|
50
|
+
persistModelSelection(model, { configHomeDir });
|
|
51
|
+
return `Model: ${model.name || model.id} (${model.provider})`;
|
|
52
|
+
}
|
|
53
|
+
if (parsed.type === "select") return selectModelByIndex(parsed.index, { runner });
|
|
54
|
+
if (parsed.type === "error") return `Error: ${parsed.message}`;
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function modelSelectSearchText(item) {
|
|
59
|
+
const model = item?.model;
|
|
60
|
+
return `${item?.label ?? ""} ${model?.name ?? ""} ${model?.id ?? ""} ${model?.provider ?? ""}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function persistModelSelection(model, { configHomeDir } = {}) {
|
|
64
|
+
if (!model?.provider || !model?.id) return null;
|
|
65
|
+
return upsertModelSelection({
|
|
66
|
+
path: globalConfigJsonPath(configHomeDir),
|
|
67
|
+
provider: model.provider,
|
|
68
|
+
model: model.__isFast ? model.__baseId : model.id,
|
|
69
|
+
serviceTier: model.__isFast ? "priority" : undefined,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatModelsList({ current, scopedModels = [] }) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
if (current) {
|
|
76
|
+
lines.push(`Current: ${current.name || current.id} (${current.provider})`);
|
|
77
|
+
}
|
|
78
|
+
if (scopedModels.length === 0) {
|
|
79
|
+
lines.push("(no available models - run `march provider --config`)");
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
lines.push(...formatGroupedModels({ current, scopedModels }));
|
|
83
|
+
lines.push("Use Ctrl+L or /model to choose a model.");
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatGroupedModels({ current, scopedModels }) {
|
|
88
|
+
const groups = new Map();
|
|
89
|
+
for (const item of scopedModels) {
|
|
90
|
+
const provider = item.model.provider;
|
|
91
|
+
if (!groups.has(provider)) groups.set(provider, []);
|
|
92
|
+
groups.get(provider).push(item);
|
|
93
|
+
}
|
|
94
|
+
const lines = [];
|
|
95
|
+
for (const [provider, items] of [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
96
|
+
lines.push(`── ${getProviderLabel(provider)} ──`);
|
|
97
|
+
for (const { model } of items) {
|
|
98
|
+
const marker = current && model.id === current.id && model.provider === current.provider ? "●" : " ";
|
|
99
|
+
lines.push(` ${marker} ${model.name || model.id}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function listModels({ runner }) {
|
|
106
|
+
return formatModelsList({
|
|
107
|
+
current: runner.getCurrentModel?.(),
|
|
108
|
+
scopedModels: runner.getScopedModels?.() || [],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readClipboardImage } from "../input/image-clipboard.mjs";
|
|
2
|
+
import { saveImageAttachment } from "../../session/attachments.mjs";
|
|
3
|
+
import { formatAttachmentMarkerForDisplay } from "../../session/attachment-display.mjs";
|
|
4
|
+
|
|
5
|
+
export function pasteClipboardImage({
|
|
6
|
+
ui,
|
|
7
|
+
projectMarchDir,
|
|
8
|
+
sessionId,
|
|
9
|
+
readClipboardImageImpl = readClipboardImage,
|
|
10
|
+
saveImageAttachmentImpl = saveImageAttachment,
|
|
11
|
+
now = new Date(),
|
|
12
|
+
} = {}) {
|
|
13
|
+
const image = readClipboardImageImpl();
|
|
14
|
+
if (!image?.ok) return [`Error: ${image?.message || "failed to read clipboard image"}`];
|
|
15
|
+
|
|
16
|
+
let saved;
|
|
17
|
+
try {
|
|
18
|
+
saved = saveImageAttachmentImpl({
|
|
19
|
+
projectMarchDir,
|
|
20
|
+
sessionId,
|
|
21
|
+
data: image.data,
|
|
22
|
+
mimeType: image.mimeType,
|
|
23
|
+
source: "clipboard",
|
|
24
|
+
now,
|
|
25
|
+
});
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return [`Error: failed to save clipboard image: ${err.message}`];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const marker = `@.march/${saved.relativePath}`;
|
|
31
|
+
const displayLabel = formatAttachmentMarkerForDisplay(marker);
|
|
32
|
+
if (typeof ui?.insertAttachmentAtCursor === "function") {
|
|
33
|
+
ui.insertAttachmentAtCursor({ marker, label: displayLabel });
|
|
34
|
+
} else {
|
|
35
|
+
ui?.insertTextAtCursor?.(withLeadingSpace(ui?.getInputText?.() ?? "", marker));
|
|
36
|
+
}
|
|
37
|
+
return [`Attached image: ${displayLabel}`];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function withLeadingSpace(currentText, marker) {
|
|
41
|
+
if (!String(currentText || "").trim()) return marker;
|
|
42
|
+
return ` ${marker}`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getProviderLabel } from "../../provider/presets.mjs";
|
|
2
|
+
|
|
3
|
+
export function parseProviderCommand(input) {
|
|
4
|
+
if (input !== "/providers" && !input.startsWith("/providers ")) {
|
|
5
|
+
return { type: "none" };
|
|
6
|
+
}
|
|
7
|
+
const arg = input.slice("/providers".length).trim();
|
|
8
|
+
if (!arg) return { type: "list" };
|
|
9
|
+
return { type: "error", message: "Provider switching is done by choosing a model with Ctrl+L or /model." };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function listProviders({ runner }) {
|
|
13
|
+
const scopedModels = runner.getScopedModels?.() || [];
|
|
14
|
+
const providers = new Map();
|
|
15
|
+
for (const { model } of scopedModels) {
|
|
16
|
+
if (!providers.has(model.provider)) {
|
|
17
|
+
providers.set(model.provider, {
|
|
18
|
+
provider: model.provider,
|
|
19
|
+
modelCount: 0,
|
|
20
|
+
defaultModel: model.id,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const entry = providers.get(model.provider);
|
|
24
|
+
entry.modelCount += 1;
|
|
25
|
+
}
|
|
26
|
+
return [...providers.values()].sort((a, b) => a.provider.localeCompare(b.provider));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatProvidersList({ currentProvider, providers = [] }) {
|
|
30
|
+
if (providers.length === 0) return ["(no providers available)"];
|
|
31
|
+
return providers.map(({ provider, modelCount }) => {
|
|
32
|
+
const marker = provider === currentProvider ? "●" : "○";
|
|
33
|
+
return `${marker} ${provider} (${modelCount} model${modelCount === 1 ? "" : "s"})`;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function handleProviderCommand(parsed, { ui, runner }) {
|
|
38
|
+
if (parsed.type === "list") {
|
|
39
|
+
const configured = runner.getConfiguredProviders?.() ?? [];
|
|
40
|
+
if (configured.length === 0) {
|
|
41
|
+
return [
|
|
42
|
+
"Configured providers: (none)",
|
|
43
|
+
"Run `march provider --config` outside REPL to add credentials.",
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
"Configured providers:",
|
|
48
|
+
...configured.map((provider) => ` ${getProviderLabel(provider)}`),
|
|
49
|
+
"Use Ctrl+L or /model to choose a model.",
|
|
50
|
+
"Run `march provider --config` outside REPL to add/update credentials.",
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
if (parsed.type === "error") return `Error: ${parsed.message}`;
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { MODES, formatModeLabel } from "../input/mode-state.mjs";
|
|
3
|
+
import { accent, text, PREFIX, R } from "../tui/ui-theme.mjs";
|
|
4
|
+
|
|
5
|
+
export function statusCommand({
|
|
6
|
+
runner,
|
|
7
|
+
sessionState,
|
|
8
|
+
sessionSource = "pi",
|
|
9
|
+
extensionDiagnostics = [],
|
|
10
|
+
lifecycleState = null,
|
|
11
|
+
gitBranch = getGitBranch(runner.engine.cwd),
|
|
12
|
+
}) {
|
|
13
|
+
return [formatStatusLine({
|
|
14
|
+
engine: runner.engine,
|
|
15
|
+
sessionState,
|
|
16
|
+
sessionStats: runner.getSessionStats?.() ?? null,
|
|
17
|
+
sessionSource,
|
|
18
|
+
extensionDiagnostics,
|
|
19
|
+
lifecycleState,
|
|
20
|
+
gitBranch,
|
|
21
|
+
})];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function statusBarLine({
|
|
25
|
+
runner,
|
|
26
|
+
sessionState,
|
|
27
|
+
sessionSource = "pi",
|
|
28
|
+
extensionDiagnostics = [],
|
|
29
|
+
lifecycleState = null,
|
|
30
|
+
gitBranch = getGitBranch(runner.engine.cwd),
|
|
31
|
+
mode = MODES.DO,
|
|
32
|
+
contextTokens = null,
|
|
33
|
+
activity = null,
|
|
34
|
+
}) {
|
|
35
|
+
return formatStatusBarLine({
|
|
36
|
+
engine: runner.engine,
|
|
37
|
+
sessionState,
|
|
38
|
+
sessionStats: runner.getSessionStats?.() ?? null,
|
|
39
|
+
sessionSource,
|
|
40
|
+
extensionDiagnostics,
|
|
41
|
+
lifecycleState,
|
|
42
|
+
gitBranch,
|
|
43
|
+
mode,
|
|
44
|
+
contextTokens,
|
|
45
|
+
activity,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatStatusLine({
|
|
50
|
+
engine,
|
|
51
|
+
sessionState,
|
|
52
|
+
sessionStats = null,
|
|
53
|
+
sessionSource = "pi",
|
|
54
|
+
extensionDiagnostics = [],
|
|
55
|
+
lifecycleState = null,
|
|
56
|
+
gitBranch = null,
|
|
57
|
+
}) {
|
|
58
|
+
const statsSessionId = sessionStats?.sessionId ?? sessionState?.sessionId ?? "unknown";
|
|
59
|
+
const tokens = sessionStats?.tokens
|
|
60
|
+
? `${sessionStats.tokens.input ?? 0}in/${sessionStats.tokens.output ?? 0}out`
|
|
61
|
+
: "n/a";
|
|
62
|
+
const parts = [
|
|
63
|
+
`git:${gitBranch || "none"}`,
|
|
64
|
+
`session:${statsSessionId}`,
|
|
65
|
+
`source:${sessionSource}`,
|
|
66
|
+
];
|
|
67
|
+
if (engine.sessionName) parts.push(`name:${engine.sessionName}`);
|
|
68
|
+
parts.push(
|
|
69
|
+
`model:${engine.modelId}`,
|
|
70
|
+
`provider:${engine.provider}`,
|
|
71
|
+
`thinking:${engine.thinkingLevel ?? "unknown"}`,
|
|
72
|
+
`tokens:${tokens}`,
|
|
73
|
+
`ext:${formatExtensionDiagnosticSummary(extensionDiagnostics, lifecycleState)}`,
|
|
74
|
+
);
|
|
75
|
+
return parts.join(" ");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function formatStatusBarLine({
|
|
79
|
+
engine,
|
|
80
|
+
mode = MODES.DO,
|
|
81
|
+
contextTokens = null,
|
|
82
|
+
activity = null,
|
|
83
|
+
}) {
|
|
84
|
+
const model = engine.modelId || "model?";
|
|
85
|
+
const thinking = engine.thinkingLevel || "thinking?";
|
|
86
|
+
|
|
87
|
+
const C = PREFIX; // foreground-only color prefixes (no reset)
|
|
88
|
+
const DIM = C.brightBlack;
|
|
89
|
+
const OK = "\x1b[32m"; // green, no reset
|
|
90
|
+
const WARN = "\x1b[33m"; // yellow, no reset
|
|
91
|
+
const modeSegment = `${mode === MODES.DISCUSS ? WARN : OK}${formatModeLabel(mode)}`;
|
|
92
|
+
const runtime = `${C.cyan}${model}${DIM}·${thinking}`;
|
|
93
|
+
const segments = [modeSegment, runtime];
|
|
94
|
+
const activityText = formatActivitySegment(activity);
|
|
95
|
+
if (activityText) segments.push(`${C.fg250}${activityText}`);
|
|
96
|
+
const compactTokens = formatCompactTokenCount(contextTokens);
|
|
97
|
+
if (compactTokens) segments.push(`${C.fg250}${compactTokens}`);
|
|
98
|
+
|
|
99
|
+
const inner = segments.join(` ${DIM}|${C.fg250} `);
|
|
100
|
+
return `${inner}${R}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatActivitySegment(activity) {
|
|
104
|
+
if (!activity) return "";
|
|
105
|
+
const label = String(activity.label ?? "").trim();
|
|
106
|
+
const frame = String(activity.frame ?? "").trim();
|
|
107
|
+
return [frame, label].filter(Boolean).join(" ");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function formatCompactTokenCount(tokens) {
|
|
111
|
+
const value = Number(tokens);
|
|
112
|
+
if (!Number.isFinite(value) || value <= 0) return "";
|
|
113
|
+
if (value < 1000) return String(Math.ceil(value));
|
|
114
|
+
if (value < 1000000) return `${formatOneDecimal(value / 1000)}K`;
|
|
115
|
+
return `${formatOneDecimal(value / 1000000)}M`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function formatExtensionDiagnosticSummary(extensionDiagnostics = [], lifecycleState = null) {
|
|
119
|
+
const diagnostics = [...extensionDiagnostics, ...(lifecycleState?.diagnostics ?? [])];
|
|
120
|
+
if (diagnostics.length === 0) return "ok";
|
|
121
|
+
const counts = new Map();
|
|
122
|
+
for (const diagnostic of diagnostics) {
|
|
123
|
+
const type = diagnostic?.type ?? "info";
|
|
124
|
+
counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
125
|
+
}
|
|
126
|
+
return [...counts.entries()]
|
|
127
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
128
|
+
.map(([type, count]) => `${count}${type}`)
|
|
129
|
+
.join(",");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getGitBranch(cwd) {
|
|
133
|
+
const branch = runGit(cwd, ["branch", "--show-current"]);
|
|
134
|
+
if (branch) return branch;
|
|
135
|
+
return runGit(cwd, ["rev-parse", "--short", "HEAD"]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function shortSessionId(sessionId) {
|
|
139
|
+
const value = String(sessionId || "unknown");
|
|
140
|
+
if (value === "unknown" || value.length <= 8) return value;
|
|
141
|
+
return value.slice(0, 8);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function runGit(cwd, args) {
|
|
145
|
+
const result = spawnSync("git", args, {
|
|
146
|
+
cwd,
|
|
147
|
+
encoding: "utf8",
|
|
148
|
+
windowsHide: true,
|
|
149
|
+
});
|
|
150
|
+
if (result.status !== 0) return null;
|
|
151
|
+
return result.stdout.trim() || null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatOneDecimal(value) {
|
|
155
|
+
const rounded = Math.round(value * 10) / 10;
|
|
156
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
157
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { formatSelectorList } from "../selector-list.mjs";
|
|
2
|
+
|
|
3
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
4
|
+
|
|
5
|
+
export function parseThinkingCommand(input) {
|
|
6
|
+
if (input !== "/thinking" && !input.startsWith("/thinking ")) {
|
|
7
|
+
return { type: "none" };
|
|
8
|
+
}
|
|
9
|
+
const arg = input.slice("/thinking".length).trim();
|
|
10
|
+
if (!arg) return { type: "select-interactive" };
|
|
11
|
+
if (arg === "list") return { type: "list" };
|
|
12
|
+
const index = Number(arg);
|
|
13
|
+
if (Number.isInteger(index) && index > 0) return { type: "select", index };
|
|
14
|
+
if (THINKING_LEVELS.includes(arg)) return { type: "set", level: arg };
|
|
15
|
+
return {
|
|
16
|
+
type: "error",
|
|
17
|
+
message: `Usage: /thinking [list|index|${THINKING_LEVELS.join("|")}]`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatThinkingLevels(levels, current) {
|
|
22
|
+
const available = Array.isArray(levels) && levels.length > 0 ? levels : THINKING_LEVELS;
|
|
23
|
+
return formatSelectorList({
|
|
24
|
+
items: available,
|
|
25
|
+
currentIndex: available.indexOf(current),
|
|
26
|
+
instruction: "Use /thinking <index> to select.",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildThinkingSelectItems(levels, current) {
|
|
31
|
+
const available = Array.isArray(levels) && levels.length > 0 ? levels : THINKING_LEVELS;
|
|
32
|
+
return available.map((level, index) => ({
|
|
33
|
+
value: String(index),
|
|
34
|
+
label: level,
|
|
35
|
+
description: level === current ? "current" : "",
|
|
36
|
+
level,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function selectThinkingByIndex(index, { runner }) {
|
|
41
|
+
const levels = runner.getAvailableThinkingLevels?.() || THINKING_LEVELS;
|
|
42
|
+
const level = levels[index - 1];
|
|
43
|
+
if (!level) return `Error: thinking index out of range: ${index}`;
|
|
44
|
+
return `thinking: ${runner.setThinkingLevel(level)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function handleThinkingCommand(parsed, { runner, ui = null } = {}) {
|
|
48
|
+
if (parsed.type === "select-interactive") {
|
|
49
|
+
if (ui?.selectList) return [await selectThinkingInteractively({ runner, ui })];
|
|
50
|
+
return ["Use /thinking list or run in TUI to choose a thinking level."];
|
|
51
|
+
}
|
|
52
|
+
if (parsed.type === "list") {
|
|
53
|
+
return formatThinkingLevels(
|
|
54
|
+
runner.getAvailableThinkingLevels?.(),
|
|
55
|
+
runner.getThinkingLevel?.(),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (parsed.type === "select") return [selectThinkingByIndex(parsed.index, { runner })];
|
|
59
|
+
if (parsed.type === "set") {
|
|
60
|
+
const level = runner.setThinkingLevel(parsed.level);
|
|
61
|
+
return [`thinking: ${level}`];
|
|
62
|
+
}
|
|
63
|
+
if (parsed.type === "error") return [`Error: ${parsed.message}`];
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function selectThinkingInteractively({ runner, ui }) {
|
|
68
|
+
const levels = runner.getAvailableThinkingLevels?.() || THINKING_LEVELS;
|
|
69
|
+
if (levels.length === 0) return "thinking: no available levels";
|
|
70
|
+
const current = runner.getThinkingLevel?.();
|
|
71
|
+
const selectedIndex = Math.max(0, levels.indexOf(current));
|
|
72
|
+
const item = await ui.selectList({
|
|
73
|
+
items: buildThinkingSelectItems(levels, current),
|
|
74
|
+
selectedIndex,
|
|
75
|
+
width: 48,
|
|
76
|
+
suppressInitialConfirm: true,
|
|
77
|
+
});
|
|
78
|
+
if (!item) return "thinking: unchanged";
|
|
79
|
+
return `thinking: ${runner.setThinkingLevel(item.level)}`;
|
|
80
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { stdout } from "node:process";
|
|
2
|
+
import { extractToolOutput } from "./tool-output.mjs";
|
|
3
|
+
import { formatMemoryHintLines } from "./tui/recall-rendering.mjs";
|
|
4
|
+
import { formatToolStartLine } from "./tui/tool-rendering.mjs";
|
|
5
|
+
import { brightBlack, dim, red, green, yellow } from "./tui/ui-theme.mjs";
|
|
6
|
+
|
|
7
|
+
export function createJsonUI() {
|
|
8
|
+
let thinkingBuf = "";
|
|
9
|
+
return {
|
|
10
|
+
readline: () => Promise.resolve(""),
|
|
11
|
+
write: () => {},
|
|
12
|
+
writeln: (text) => {
|
|
13
|
+
stdout.write(text + "\n");
|
|
14
|
+
},
|
|
15
|
+
thinkingStart: () => { thinkingBuf = ""; },
|
|
16
|
+
thinkingDelta: (delta) => { thinkingBuf += delta; },
|
|
17
|
+
thinkingEnd: (tokens) => {
|
|
18
|
+
stdout.write(JSON.stringify({ type: "thinking", tokens, content: thinkingBuf }) + "\n");
|
|
19
|
+
thinkingBuf = "";
|
|
20
|
+
},
|
|
21
|
+
thinkingBlock: (tokens, content) => {
|
|
22
|
+
stdout.write(JSON.stringify({ type: "thinking", tokens, content }) + "\n");
|
|
23
|
+
},
|
|
24
|
+
toggleLastThinking: () => {},
|
|
25
|
+
toolStart: (name, args) => {
|
|
26
|
+
stdout.write(JSON.stringify({ type: "tool_start", name, args }) + "\n");
|
|
27
|
+
},
|
|
28
|
+
toolEnd: (name, isError, result) => {
|
|
29
|
+
stdout.write(JSON.stringify({ type: "tool_end", name, isError, output: extractToolOutput(result) }) + "\n");
|
|
30
|
+
},
|
|
31
|
+
textDelta: (delta) => {
|
|
32
|
+
stdout.write(delta);
|
|
33
|
+
},
|
|
34
|
+
status: () => {},
|
|
35
|
+
memoryHint: () => {},
|
|
36
|
+
clearOutput: () => {},
|
|
37
|
+
restoreTranscript: () => {},
|
|
38
|
+
setStatusBar: () => {},
|
|
39
|
+
turnStart: () => {},
|
|
40
|
+
assistantReplyEnd: () => {},
|
|
41
|
+
turnEnd: () => {},
|
|
42
|
+
retryStart: (event) => {
|
|
43
|
+
stdout.write(JSON.stringify({ type: "retry_start", ...event }) + "\n");
|
|
44
|
+
},
|
|
45
|
+
retryEnd: (event) => {
|
|
46
|
+
stdout.write(JSON.stringify({ type: "retry_end", ...event }) + "\n");
|
|
47
|
+
},
|
|
48
|
+
editDiff: (path, diffLines) => {
|
|
49
|
+
stdout.write(JSON.stringify({ type: "edit_diff", path, diff: diffLines }) + "\n");
|
|
50
|
+
},
|
|
51
|
+
requestPermission: async () => true,
|
|
52
|
+
setEscapeHandler: () => {},
|
|
53
|
+
setCtrlCHandler: () => {},
|
|
54
|
+
setShiftTabHandler: () => {},
|
|
55
|
+
setCtrlTHandler: () => {},
|
|
56
|
+
setCtrlLHandler: () => {},
|
|
57
|
+
setPasteImageHandler: () => {},
|
|
58
|
+
getInputText: () => "",
|
|
59
|
+
insertTextAtCursor: () => {},
|
|
60
|
+
openExternalEditor: () => {},
|
|
61
|
+
toggleMouse: () => false,
|
|
62
|
+
toggleToolOutput: () => false,
|
|
63
|
+
requestExit: () => {},
|
|
64
|
+
close: () => {},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createPlainUI() {
|
|
69
|
+
let thinkingBuf = "";
|
|
70
|
+
let needsNewline = false;
|
|
71
|
+
const writeText = (text) => {
|
|
72
|
+
stdout.write(text);
|
|
73
|
+
needsNewline = text.length > 0 && !text.endsWith("\n");
|
|
74
|
+
};
|
|
75
|
+
const ensureNewline = () => {
|
|
76
|
+
if (needsNewline) stdout.write("\n");
|
|
77
|
+
needsNewline = false;
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
readline: (_prompt) => Promise.resolve(""),
|
|
81
|
+
write: writeText,
|
|
82
|
+
writeln: (text) => { stdout.write(text + "\n"); needsNewline = false; },
|
|
83
|
+
thinkingStart: () => { thinkingBuf = ""; },
|
|
84
|
+
thinkingDelta: (delta) => { thinkingBuf += delta; },
|
|
85
|
+
thinkingEnd: (tokens) => {
|
|
86
|
+
stdout.write(`\n${brightBlack(`--- thinking (${tokens} tokens) ---`)}\n`);
|
|
87
|
+
stdout.write(`${brightBlack(thinkingBuf)}\n`);
|
|
88
|
+
stdout.write(`${brightBlack("--- end thinking ---")}\n\n`);
|
|
89
|
+
thinkingBuf = "";
|
|
90
|
+
needsNewline = false;
|
|
91
|
+
},
|
|
92
|
+
thinkingBlock: (tokens, content) => {
|
|
93
|
+
stdout.write(`\n${brightBlack(`--- thinking (${tokens} tokens) ---`)}\n`);
|
|
94
|
+
stdout.write(`${brightBlack(content)}\n`);
|
|
95
|
+
stdout.write(`${brightBlack("--- end thinking ---")}\n\n`);
|
|
96
|
+
needsNewline = false;
|
|
97
|
+
},
|
|
98
|
+
toggleLastThinking: () => {},
|
|
99
|
+
toolStart: (name, args) => {
|
|
100
|
+
stdout.write(`${dim(` ${formatToolStartLine(name, args)}`)}\n`);
|
|
101
|
+
},
|
|
102
|
+
toolEnd: (name, isError, result) => {
|
|
103
|
+
const out = extractToolOutput(result);
|
|
104
|
+
if (isError) {
|
|
105
|
+
stdout.write(`${red(` ◆ ${name} failed`)}\n`);
|
|
106
|
+
if (out) stdout.write(`${red(` ${out.slice(0, 200)}`)}\n`);
|
|
107
|
+
} else if (out) {
|
|
108
|
+
stdout.write(`${dim(` ${out.split("\n")[0].slice(0, 200)}`)}\n`);
|
|
109
|
+
}
|
|
110
|
+
needsNewline = false;
|
|
111
|
+
},
|
|
112
|
+
textDelta: writeText,
|
|
113
|
+
status: (text) => { ensureNewline(); stdout.write(`${brightBlack(`● ${text}`)}\n`); },
|
|
114
|
+
memoryHint: ({ hints }) => {
|
|
115
|
+
ensureNewline();
|
|
116
|
+
for (const line of formatMemoryHintLines(hints)) stdout.write(`${brightBlack(line)}\n`);
|
|
117
|
+
},
|
|
118
|
+
clearOutput: () => {},
|
|
119
|
+
restoreTranscript: () => {},
|
|
120
|
+
setStatusBar: () => {},
|
|
121
|
+
turnStart: () => {},
|
|
122
|
+
assistantReplyEnd: ensureNewline,
|
|
123
|
+
turnEnd: ensureNewline,
|
|
124
|
+
retryStart: ({ attempt, maxAttempts, delayMs, errorMessage }) => {
|
|
125
|
+
ensureNewline();
|
|
126
|
+
stdout.write(`${yellow(`● retrying (${attempt}/${maxAttempts}) in ${Math.ceil(delayMs / 1000)}s: ${errorMessage || "Unknown error"}`)}\n`);
|
|
127
|
+
},
|
|
128
|
+
retryEnd: ({ success, attempt, finalError }) => {
|
|
129
|
+
ensureNewline();
|
|
130
|
+
const status = success ? "recovered" : "stopped";
|
|
131
|
+
stdout.write(`${brightBlack(`● retry ${status} after ${attempt} attempt${attempt === 1 ? "" : "s"}${finalError ? `: ${finalError}` : ""}`)}\n`);
|
|
132
|
+
},
|
|
133
|
+
editDiff: (path, diffLines) => {
|
|
134
|
+
stdout.write(`\n${dim(` ± ${path}`)}\n`);
|
|
135
|
+
for (const d of diffLines) {
|
|
136
|
+
if (d.type === "del") stdout.write(`${red(` - ${d.text}`)}\n`);
|
|
137
|
+
else if (d.type === "add") stdout.write(`${green(` + ${d.text}`)}\n`);
|
|
138
|
+
else stdout.write(`${dim(` ${d.text}`)}\n`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
requestPermission: async () => true,
|
|
142
|
+
setEscapeHandler: () => {},
|
|
143
|
+
setCtrlCHandler: () => {},
|
|
144
|
+
setShiftTabHandler: () => {},
|
|
145
|
+
setCtrlTHandler: () => {},
|
|
146
|
+
setCtrlLHandler: () => {},
|
|
147
|
+
setPasteImageHandler: () => {},
|
|
148
|
+
getInputText: () => "",
|
|
149
|
+
insertTextAtCursor: () => {},
|
|
150
|
+
openExternalEditor: () => {},
|
|
151
|
+
toggleMouse: () => false,
|
|
152
|
+
toggleToolOutput: () => false,
|
|
153
|
+
requestExit: () => {},
|
|
154
|
+
close: () => {},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function resolveAttachmentTokens(text, tokens) {
|
|
2
|
+
let resolved = text;
|
|
3
|
+
for (const [token, marker] of tokens) {
|
|
4
|
+
resolved = resolved.split(token).join(marker);
|
|
5
|
+
}
|
|
6
|
+
return resolved;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function uniqueAttachmentToken(label, tokens) {
|
|
10
|
+
if (!tokens.has(label)) return label;
|
|
11
|
+
for (let i = 2; ; i++) {
|
|
12
|
+
const candidate = label.replace(/\]$/, ` ${i}]`);
|
|
13
|
+
if (!tokens.has(candidate)) return candidate;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function withLeadingSpace(currentText, text) {
|
|
18
|
+
if (!String(currentText || "").trim()) return text;
|
|
19
|
+
return ` ${text}`;
|
|
20
|
+
}
|