indusagi-coding-agent 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/CHANGELOG.md +2249 -0
- package/README.md +546 -0
- package/dist/cli/args.js +282 -0
- package/dist/cli/config-selector.js +30 -0
- package/dist/cli/file-processor.js +78 -0
- package/dist/cli/list-models.js +91 -0
- package/dist/cli/session-picker.js +31 -0
- package/dist/cli.js +10 -0
- package/dist/config.js +158 -0
- package/dist/core/agent-session.js +2097 -0
- package/dist/core/auth-storage.js +278 -0
- package/dist/core/bash-executor.js +211 -0
- package/dist/core/compaction/branch-summarization.js +241 -0
- package/dist/core/compaction/compaction.js +606 -0
- package/dist/core/compaction/index.js +6 -0
- package/dist/core/compaction/utils.js +137 -0
- package/dist/core/diagnostics.js +1 -0
- package/dist/core/event-bus.js +24 -0
- package/dist/core/exec.js +70 -0
- package/dist/core/export-html/ansi-to-html.js +248 -0
- package/dist/core/export-html/index.js +221 -0
- package/dist/core/export-html/template.css +905 -0
- package/dist/core/export-html/template.html +54 -0
- package/dist/core/export-html/template.js +1549 -0
- package/dist/core/export-html/tool-renderer.js +56 -0
- package/dist/core/export-html/vendor/highlight.min.js +1213 -0
- package/dist/core/export-html/vendor/marked.min.js +6 -0
- package/dist/core/extensions/index.js +8 -0
- package/dist/core/extensions/loader.js +395 -0
- package/dist/core/extensions/runner.js +499 -0
- package/dist/core/extensions/types.js +31 -0
- package/dist/core/extensions/wrapper.js +101 -0
- package/dist/core/footer-data-provider.js +133 -0
- package/dist/core/index.js +8 -0
- package/dist/core/keybindings.js +140 -0
- package/dist/core/messages.js +122 -0
- package/dist/core/model-registry.js +454 -0
- package/dist/core/model-resolver.js +309 -0
- package/dist/core/package-manager.js +1142 -0
- package/dist/core/prompt-templates.js +250 -0
- package/dist/core/resource-loader.js +569 -0
- package/dist/core/sdk.js +225 -0
- package/dist/core/session-manager.js +1078 -0
- package/dist/core/settings-manager.js +430 -0
- package/dist/core/skills.js +339 -0
- package/dist/core/system-prompt.js +136 -0
- package/dist/core/timings.js +24 -0
- package/dist/core/tools/bash.js +226 -0
- package/dist/core/tools/edit-diff.js +242 -0
- package/dist/core/tools/edit.js +145 -0
- package/dist/core/tools/find.js +205 -0
- package/dist/core/tools/grep.js +238 -0
- package/dist/core/tools/index.js +60 -0
- package/dist/core/tools/ls.js +117 -0
- package/dist/core/tools/path-utils.js +52 -0
- package/dist/core/tools/read.js +165 -0
- package/dist/core/tools/truncate.js +204 -0
- package/dist/core/tools/write.js +77 -0
- package/dist/index.js +41 -0
- package/dist/main.js +565 -0
- package/dist/migrations.js +260 -0
- package/dist/modes/index.js +7 -0
- package/dist/modes/interactive/components/armin.js +328 -0
- package/dist/modes/interactive/components/assistant-message.js +86 -0
- package/dist/modes/interactive/components/bash-execution.js +155 -0
- package/dist/modes/interactive/components/bordered-loader.js +47 -0
- package/dist/modes/interactive/components/branch-summary-message.js +41 -0
- package/dist/modes/interactive/components/compaction-summary-message.js +42 -0
- package/dist/modes/interactive/components/config-selector.js +458 -0
- package/dist/modes/interactive/components/countdown-timer.js +27 -0
- package/dist/modes/interactive/components/custom-editor.js +61 -0
- package/dist/modes/interactive/components/custom-message.js +80 -0
- package/dist/modes/interactive/components/diff.js +132 -0
- package/dist/modes/interactive/components/dynamic-border.js +19 -0
- package/dist/modes/interactive/components/extension-editor.js +96 -0
- package/dist/modes/interactive/components/extension-input.js +54 -0
- package/dist/modes/interactive/components/extension-selector.js +70 -0
- package/dist/modes/interactive/components/footer.js +213 -0
- package/dist/modes/interactive/components/index.js +31 -0
- package/dist/modes/interactive/components/keybinding-hints.js +60 -0
- package/dist/modes/interactive/components/login-dialog.js +138 -0
- package/dist/modes/interactive/components/model-selector.js +253 -0
- package/dist/modes/interactive/components/oauth-selector.js +91 -0
- package/dist/modes/interactive/components/scoped-models-selector.js +262 -0
- package/dist/modes/interactive/components/session-selector-search.js +145 -0
- package/dist/modes/interactive/components/session-selector.js +698 -0
- package/dist/modes/interactive/components/settings-selector.js +250 -0
- package/dist/modes/interactive/components/show-images-selector.js +33 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +44 -0
- package/dist/modes/interactive/components/theme-selector.js +43 -0
- package/dist/modes/interactive/components/thinking-selector.js +45 -0
- package/dist/modes/interactive/components/tool-execution.js +608 -0
- package/dist/modes/interactive/components/tree-selector.js +892 -0
- package/dist/modes/interactive/components/user-message-selector.js +109 -0
- package/dist/modes/interactive/components/user-message.js +15 -0
- package/dist/modes/interactive/components/visual-truncate.js +32 -0
- package/dist/modes/interactive/interactive-mode.js +3576 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.js +938 -0
- package/dist/modes/print-mode.js +96 -0
- package/dist/modes/rpc/rpc-client.js +390 -0
- package/dist/modes/rpc/rpc-mode.js +448 -0
- package/dist/modes/rpc/rpc-types.js +7 -0
- package/dist/utils/changelog.js +86 -0
- package/dist/utils/clipboard-image.js +116 -0
- package/dist/utils/clipboard.js +58 -0
- package/dist/utils/frontmatter.js +25 -0
- package/dist/utils/git.js +5 -0
- package/dist/utils/image-convert.js +34 -0
- package/dist/utils/image-resize.js +180 -0
- package/dist/utils/mime.js +25 -0
- package/dist/utils/photon.js +120 -0
- package/dist/utils/shell.js +164 -0
- package/dist/utils/sleep.js +16 -0
- package/dist/utils/tools-manager.js +186 -0
- package/docs/compaction.md +390 -0
- package/docs/custom-provider.md +538 -0
- package/docs/development.md +69 -0
- package/docs/extensions.md +1733 -0
- package/docs/images/doom-extension.png +0 -0
- package/docs/images/interactive-mode.png +0 -0
- package/docs/images/tree-view.png +0 -0
- package/docs/json.md +79 -0
- package/docs/keybindings.md +162 -0
- package/docs/models.md +193 -0
- package/docs/packages.md +163 -0
- package/docs/prompt-templates.md +67 -0
- package/docs/providers.md +147 -0
- package/docs/rpc.md +1048 -0
- package/docs/sdk.md +957 -0
- package/docs/session.md +412 -0
- package/docs/settings.md +216 -0
- package/docs/shell-aliases.md +13 -0
- package/docs/skills.md +226 -0
- package/docs/terminal-setup.md +65 -0
- package/docs/themes.md +295 -0
- package/docs/tree.md +219 -0
- package/docs/tui.md +887 -0
- package/docs/windows.md +17 -0
- package/examples/README.md +25 -0
- package/examples/extensions/README.md +192 -0
- package/examples/extensions/antigravity-image-gen.ts +414 -0
- package/examples/extensions/auto-commit-on-exit.ts +49 -0
- package/examples/extensions/bookmark.ts +50 -0
- package/examples/extensions/claude-rules.ts +86 -0
- package/examples/extensions/confirm-destructive.ts +59 -0
- package/examples/extensions/custom-compaction.ts +115 -0
- package/examples/extensions/custom-footer.ts +65 -0
- package/examples/extensions/custom-header.ts +73 -0
- package/examples/extensions/custom-provider-anthropic/index.ts +605 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
- package/examples/extensions/custom-provider-anthropic/package.json +19 -0
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +350 -0
- package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +83 -0
- package/examples/extensions/dirty-repo-guard.ts +56 -0
- package/examples/extensions/doom-overlay/README.md +46 -0
- package/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
- package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/examples/extensions/doom-overlay/doom/build.sh +152 -0
- package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
- package/examples/extensions/doom-overlay/doom-component.ts +133 -0
- package/examples/extensions/doom-overlay/doom-engine.ts +173 -0
- package/examples/extensions/doom-overlay/doom-keys.ts +105 -0
- package/examples/extensions/doom-overlay/index.ts +74 -0
- package/examples/extensions/doom-overlay/wad-finder.ts +51 -0
- package/examples/extensions/event-bus.ts +43 -0
- package/examples/extensions/file-trigger.ts +41 -0
- package/examples/extensions/git-checkpoint.ts +53 -0
- package/examples/extensions/handoff.ts +151 -0
- package/examples/extensions/hello.ts +25 -0
- package/examples/extensions/inline-bash.ts +94 -0
- package/examples/extensions/input-transform.ts +43 -0
- package/examples/extensions/interactive-shell.ts +196 -0
- package/examples/extensions/mac-system-theme.ts +47 -0
- package/examples/extensions/message-renderer.ts +60 -0
- package/examples/extensions/modal-editor.ts +86 -0
- package/examples/extensions/model-status.ts +31 -0
- package/examples/extensions/notify.ts +25 -0
- package/examples/extensions/overlay-qa-tests.ts +882 -0
- package/examples/extensions/overlay-test.ts +151 -0
- package/examples/extensions/permission-gate.ts +34 -0
- package/examples/extensions/pirate.ts +47 -0
- package/examples/extensions/plan-mode/README.md +65 -0
- package/examples/extensions/plan-mode/index.ts +341 -0
- package/examples/extensions/plan-mode/utils.ts +168 -0
- package/examples/extensions/preset.ts +399 -0
- package/examples/extensions/protected-paths.ts +30 -0
- package/examples/extensions/qna.ts +120 -0
- package/examples/extensions/question.ts +265 -0
- package/examples/extensions/questionnaire.ts +428 -0
- package/examples/extensions/rainbow-editor.ts +88 -0
- package/examples/extensions/sandbox/index.ts +318 -0
- package/examples/extensions/sandbox/package-lock.json +92 -0
- package/examples/extensions/sandbox/package.json +19 -0
- package/examples/extensions/send-user-message.ts +97 -0
- package/examples/extensions/session-name.ts +27 -0
- package/examples/extensions/shutdown-command.ts +63 -0
- package/examples/extensions/snake.ts +344 -0
- package/examples/extensions/space-invaders.ts +561 -0
- package/examples/extensions/ssh.ts +220 -0
- package/examples/extensions/status-line.ts +40 -0
- package/examples/extensions/subagent/README.md +172 -0
- package/examples/extensions/subagent/agents/planner.md +37 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/subagent/agents/scout.md +50 -0
- package/examples/extensions/subagent/agents/worker.md +24 -0
- package/examples/extensions/subagent/agents.ts +127 -0
- package/examples/extensions/subagent/index.ts +964 -0
- package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/examples/extensions/subagent/prompts/implement.md +10 -0
- package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/examples/extensions/summarize.ts +196 -0
- package/examples/extensions/timed-confirm.ts +70 -0
- package/examples/extensions/todo.ts +300 -0
- package/examples/extensions/tool-override.ts +144 -0
- package/examples/extensions/tools.ts +147 -0
- package/examples/extensions/trigger-compact.ts +40 -0
- package/examples/extensions/truncated-tool.ts +193 -0
- package/examples/extensions/widget-placement.ts +17 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +22 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +50 -0
- package/examples/sdk/03-custom-prompt.ts +55 -0
- package/examples/sdk/04-skills.ts +46 -0
- package/examples/sdk/05-tools.ts +56 -0
- package/examples/sdk/06-extensions.ts +88 -0
- package/examples/sdk/07-context-files.ts +40 -0
- package/examples/sdk/08-prompt-templates.ts +47 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +48 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +82 -0
- package/examples/sdk/13-codex-oauth.ts +37 -0
- package/examples/sdk/README.md +144 -0
- package/package.json +85 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as Diff from "diff";
|
|
2
|
+
import { theme } from "../theme/theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parse diff line to extract prefix, line number, and content.
|
|
5
|
+
* Format: "+123 content" or "-123 content" or " 123 content" or " ..."
|
|
6
|
+
*/
|
|
7
|
+
function parseDiffLine(line) {
|
|
8
|
+
const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
|
|
9
|
+
if (!match)
|
|
10
|
+
return null;
|
|
11
|
+
return { prefix: match[1], lineNum: match[2], content: match[3] };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Replace tabs with spaces for consistent rendering.
|
|
15
|
+
*/
|
|
16
|
+
function replaceTabs(text) {
|
|
17
|
+
return text.replace(/\t/g, " ");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compute word-level diff and render with inverse on changed parts.
|
|
21
|
+
* Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.
|
|
22
|
+
* Strips leading whitespace from inverse to avoid highlighting indentation.
|
|
23
|
+
*/
|
|
24
|
+
function renderIntraLineDiff(oldContent, newContent) {
|
|
25
|
+
const wordDiff = Diff.diffWords(oldContent, newContent);
|
|
26
|
+
let removedLine = "";
|
|
27
|
+
let addedLine = "";
|
|
28
|
+
let isFirstRemoved = true;
|
|
29
|
+
let isFirstAdded = true;
|
|
30
|
+
for (const part of wordDiff) {
|
|
31
|
+
if (part.removed) {
|
|
32
|
+
let value = part.value;
|
|
33
|
+
// Strip leading whitespace from the first removed part
|
|
34
|
+
if (isFirstRemoved) {
|
|
35
|
+
const leadingWs = value.match(/^(\s*)/)?.[1] || "";
|
|
36
|
+
value = value.slice(leadingWs.length);
|
|
37
|
+
removedLine += leadingWs;
|
|
38
|
+
isFirstRemoved = false;
|
|
39
|
+
}
|
|
40
|
+
if (value) {
|
|
41
|
+
removedLine += theme.inverse(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (part.added) {
|
|
45
|
+
let value = part.value;
|
|
46
|
+
// Strip leading whitespace from the first added part
|
|
47
|
+
if (isFirstAdded) {
|
|
48
|
+
const leadingWs = value.match(/^(\s*)/)?.[1] || "";
|
|
49
|
+
value = value.slice(leadingWs.length);
|
|
50
|
+
addedLine += leadingWs;
|
|
51
|
+
isFirstAdded = false;
|
|
52
|
+
}
|
|
53
|
+
if (value) {
|
|
54
|
+
addedLine += theme.inverse(value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
removedLine += part.value;
|
|
59
|
+
addedLine += part.value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { removedLine, addedLine };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Render a diff string with colored lines and intra-line change highlighting.
|
|
66
|
+
* - Context lines: dim/gray
|
|
67
|
+
* - Removed lines: red, with inverse on changed tokens
|
|
68
|
+
* - Added lines: green, with inverse on changed tokens
|
|
69
|
+
*/
|
|
70
|
+
export function renderDiff(diffText, _options = {}) {
|
|
71
|
+
const lines = diffText.split("\n");
|
|
72
|
+
const result = [];
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < lines.length) {
|
|
75
|
+
const line = lines[i];
|
|
76
|
+
const parsed = parseDiffLine(line);
|
|
77
|
+
if (!parsed) {
|
|
78
|
+
result.push(theme.fg("toolDiffContext", line));
|
|
79
|
+
i++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (parsed.prefix === "-") {
|
|
83
|
+
// Collect consecutive removed lines
|
|
84
|
+
const removedLines = [];
|
|
85
|
+
while (i < lines.length) {
|
|
86
|
+
const p = parseDiffLine(lines[i]);
|
|
87
|
+
if (!p || p.prefix !== "-")
|
|
88
|
+
break;
|
|
89
|
+
removedLines.push({ lineNum: p.lineNum, content: p.content });
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
// Collect consecutive added lines
|
|
93
|
+
const addedLines = [];
|
|
94
|
+
while (i < lines.length) {
|
|
95
|
+
const p = parseDiffLine(lines[i]);
|
|
96
|
+
if (!p || p.prefix !== "+")
|
|
97
|
+
break;
|
|
98
|
+
addedLines.push({ lineNum: p.lineNum, content: p.content });
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
// Only do intra-line diffing when there's exactly one removed and one added line
|
|
102
|
+
// (indicating a single line modification). Otherwise, show lines as-is.
|
|
103
|
+
if (removedLines.length === 1 && addedLines.length === 1) {
|
|
104
|
+
const removed = removedLines[0];
|
|
105
|
+
const added = addedLines[0];
|
|
106
|
+
const { removedLine, addedLine } = renderIntraLineDiff(replaceTabs(removed.content), replaceTabs(added.content));
|
|
107
|
+
result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${removedLine}`));
|
|
108
|
+
result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${addedLine}`));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Show all removed lines first, then all added lines
|
|
112
|
+
for (const removed of removedLines) {
|
|
113
|
+
result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
|
|
114
|
+
}
|
|
115
|
+
for (const added of addedLines) {
|
|
116
|
+
result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (parsed.prefix === "+") {
|
|
121
|
+
// Standalone added line
|
|
122
|
+
result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Context line
|
|
127
|
+
result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result.join("\n");
|
|
132
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { theme } from "../theme/theme.js";
|
|
2
|
+
/**
|
|
3
|
+
* Dynamic border component that adjusts to viewport width.
|
|
4
|
+
*
|
|
5
|
+
* Note: When used from extensions loaded via jiti, the global `theme` may be undefined
|
|
6
|
+
* because jiti creates a separate module cache. Always pass an explicit color
|
|
7
|
+
* function when using DynamicBorder in components exported for extension use.
|
|
8
|
+
*/
|
|
9
|
+
export class DynamicBorder {
|
|
10
|
+
constructor(color = (str) => theme.fg("border", str)) {
|
|
11
|
+
this.color = color;
|
|
12
|
+
}
|
|
13
|
+
invalidate() {
|
|
14
|
+
// No cached state to invalidate currently
|
|
15
|
+
}
|
|
16
|
+
render(width) {
|
|
17
|
+
return [this.color("─".repeat(Math.max(1, width)))];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-line editor component for extensions.
|
|
3
|
+
* Supports Ctrl+G for external editor.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { Container, Editor, getEditorKeybindings, Spacer, Text, } from "indusagi/tui";
|
|
10
|
+
import { getEditorTheme, theme } from "../theme/theme.js";
|
|
11
|
+
import { DynamicBorder } from "./dynamic-border.js";
|
|
12
|
+
import { appKeyHint, keyHint } from "./keybinding-hints.js";
|
|
13
|
+
export class ExtensionEditorComponent extends Container {
|
|
14
|
+
constructor(tui, keybindings, title, prefill, onSubmit, onCancel, options) {
|
|
15
|
+
super();
|
|
16
|
+
this.tui = tui;
|
|
17
|
+
this.keybindings = keybindings;
|
|
18
|
+
this.onSubmitCallback = onSubmit;
|
|
19
|
+
this.onCancelCallback = onCancel;
|
|
20
|
+
// Add top border
|
|
21
|
+
this.addChild(new DynamicBorder());
|
|
22
|
+
this.addChild(new Spacer(1));
|
|
23
|
+
// Add title
|
|
24
|
+
this.addChild(new Text(theme.fg("accent", title), 1, 0));
|
|
25
|
+
this.addChild(new Spacer(1));
|
|
26
|
+
// Create editor
|
|
27
|
+
this.editor = new Editor(tui, getEditorTheme(), options);
|
|
28
|
+
if (prefill) {
|
|
29
|
+
this.editor.setText(prefill);
|
|
30
|
+
}
|
|
31
|
+
// Wire up Enter to submit (Shift+Enter for newlines, like the main editor)
|
|
32
|
+
this.editor.onSubmit = (text) => {
|
|
33
|
+
this.onSubmitCallback(text);
|
|
34
|
+
};
|
|
35
|
+
this.addChild(this.editor);
|
|
36
|
+
this.addChild(new Spacer(1));
|
|
37
|
+
// Add hint
|
|
38
|
+
const hasExternalEditor = !!(process.env.VISUAL || process.env.EDITOR);
|
|
39
|
+
const hint = keyHint("selectConfirm", "submit") +
|
|
40
|
+
" " +
|
|
41
|
+
keyHint("newLine", "newline") +
|
|
42
|
+
" " +
|
|
43
|
+
keyHint("selectCancel", "cancel") +
|
|
44
|
+
(hasExternalEditor ? ` ${appKeyHint(this.keybindings, "externalEditor", "external editor")}` : "");
|
|
45
|
+
this.addChild(new Text(hint, 1, 0));
|
|
46
|
+
this.addChild(new Spacer(1));
|
|
47
|
+
// Add bottom border
|
|
48
|
+
this.addChild(new DynamicBorder());
|
|
49
|
+
}
|
|
50
|
+
handleInput(keyData) {
|
|
51
|
+
const kb = getEditorKeybindings();
|
|
52
|
+
// Escape or Ctrl+C to cancel
|
|
53
|
+
if (kb.matches(keyData, "selectCancel")) {
|
|
54
|
+
this.onCancelCallback();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// External editor (app keybinding)
|
|
58
|
+
if (this.keybindings.matches(keyData, "externalEditor")) {
|
|
59
|
+
this.openExternalEditor();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Forward to editor
|
|
63
|
+
this.editor.handleInput(keyData);
|
|
64
|
+
}
|
|
65
|
+
openExternalEditor() {
|
|
66
|
+
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
|
67
|
+
if (!editorCmd) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const currentText = this.editor.getText();
|
|
71
|
+
const tmpFile = path.join(os.tmpdir(), `indusagi-extension-editor-${Date.now()}.md`);
|
|
72
|
+
try {
|
|
73
|
+
fs.writeFileSync(tmpFile, currentText, "utf-8");
|
|
74
|
+
this.tui.stop();
|
|
75
|
+
const [editor, ...editorArgs] = editorCmd.split(" ");
|
|
76
|
+
const result = spawnSync(editor, [...editorArgs, tmpFile], {
|
|
77
|
+
stdio: "inherit",
|
|
78
|
+
});
|
|
79
|
+
if (result.status === 0) {
|
|
80
|
+
const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, "");
|
|
81
|
+
this.editor.setText(newContent);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
try {
|
|
86
|
+
fs.unlinkSync(tmpFile);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Ignore cleanup errors
|
|
90
|
+
}
|
|
91
|
+
this.tui.start();
|
|
92
|
+
// Force full re-render since external editor uses alternate screen
|
|
93
|
+
this.tui.requestRender(true);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple text input component for extensions.
|
|
3
|
+
*/
|
|
4
|
+
import { Container, getEditorKeybindings, Input, Spacer, Text } from "indusagi/tui";
|
|
5
|
+
import { theme } from "../theme/theme.js";
|
|
6
|
+
import { CountdownTimer } from "./countdown-timer.js";
|
|
7
|
+
import { DynamicBorder } from "./dynamic-border.js";
|
|
8
|
+
import { keyHint } from "./keybinding-hints.js";
|
|
9
|
+
export class ExtensionInputComponent extends Container {
|
|
10
|
+
get focused() {
|
|
11
|
+
return this._focused;
|
|
12
|
+
}
|
|
13
|
+
set focused(value) {
|
|
14
|
+
this._focused = value;
|
|
15
|
+
this.input.focused = value;
|
|
16
|
+
}
|
|
17
|
+
constructor(title, _placeholder, onSubmit, onCancel, opts) {
|
|
18
|
+
super();
|
|
19
|
+
// Focusable implementation - propagate to input for IME cursor positioning
|
|
20
|
+
this._focused = false;
|
|
21
|
+
this.onSubmitCallback = onSubmit;
|
|
22
|
+
this.onCancelCallback = onCancel;
|
|
23
|
+
this.baseTitle = title;
|
|
24
|
+
this.addChild(new DynamicBorder());
|
|
25
|
+
this.addChild(new Spacer(1));
|
|
26
|
+
this.titleText = new Text(theme.fg("accent", title), 1, 0);
|
|
27
|
+
this.addChild(this.titleText);
|
|
28
|
+
this.addChild(new Spacer(1));
|
|
29
|
+
if (opts?.timeout && opts.timeout > 0 && opts.tui) {
|
|
30
|
+
this.countdown = new CountdownTimer(opts.timeout, opts.tui, (s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)), () => this.onCancelCallback());
|
|
31
|
+
}
|
|
32
|
+
this.input = new Input();
|
|
33
|
+
this.addChild(this.input);
|
|
34
|
+
this.addChild(new Spacer(1));
|
|
35
|
+
this.addChild(new Text(`${keyHint("selectConfirm", "submit")} ${keyHint("selectCancel", "cancel")}`, 1, 0));
|
|
36
|
+
this.addChild(new Spacer(1));
|
|
37
|
+
this.addChild(new DynamicBorder());
|
|
38
|
+
}
|
|
39
|
+
handleInput(keyData) {
|
|
40
|
+
const kb = getEditorKeybindings();
|
|
41
|
+
if (kb.matches(keyData, "selectConfirm") || keyData === "\n") {
|
|
42
|
+
this.onSubmitCallback(this.input.getValue());
|
|
43
|
+
}
|
|
44
|
+
else if (kb.matches(keyData, "selectCancel")) {
|
|
45
|
+
this.onCancelCallback();
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.input.handleInput(keyData);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
dispose() {
|
|
52
|
+
this.countdown?.dispose();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic selector component for extensions.
|
|
3
|
+
* Displays a list of string options with keyboard navigation.
|
|
4
|
+
*/
|
|
5
|
+
import { Container, getEditorKeybindings, Spacer, Text } from "indusagi/tui";
|
|
6
|
+
import { theme } from "../theme/theme.js";
|
|
7
|
+
import { CountdownTimer } from "./countdown-timer.js";
|
|
8
|
+
import { DynamicBorder } from "./dynamic-border.js";
|
|
9
|
+
import { keyHint, rawKeyHint } from "./keybinding-hints.js";
|
|
10
|
+
export class ExtensionSelectorComponent extends Container {
|
|
11
|
+
constructor(title, options, onSelect, onCancel, opts) {
|
|
12
|
+
super();
|
|
13
|
+
this.selectedIndex = 0;
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.onSelectCallback = onSelect;
|
|
16
|
+
this.onCancelCallback = onCancel;
|
|
17
|
+
this.baseTitle = title;
|
|
18
|
+
this.addChild(new DynamicBorder());
|
|
19
|
+
this.addChild(new Spacer(1));
|
|
20
|
+
this.titleText = new Text(theme.fg("accent", title), 1, 0);
|
|
21
|
+
this.addChild(this.titleText);
|
|
22
|
+
this.addChild(new Spacer(1));
|
|
23
|
+
if (opts?.timeout && opts.timeout > 0 && opts.tui) {
|
|
24
|
+
this.countdown = new CountdownTimer(opts.timeout, opts.tui, (s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)), () => this.onCancelCallback());
|
|
25
|
+
}
|
|
26
|
+
this.listContainer = new Container();
|
|
27
|
+
this.addChild(this.listContainer);
|
|
28
|
+
this.addChild(new Spacer(1));
|
|
29
|
+
this.addChild(new Text(rawKeyHint("↑↓", "navigate") +
|
|
30
|
+
" " +
|
|
31
|
+
keyHint("selectConfirm", "select") +
|
|
32
|
+
" " +
|
|
33
|
+
keyHint("selectCancel", "cancel"), 1, 0));
|
|
34
|
+
this.addChild(new Spacer(1));
|
|
35
|
+
this.addChild(new DynamicBorder());
|
|
36
|
+
this.updateList();
|
|
37
|
+
}
|
|
38
|
+
updateList() {
|
|
39
|
+
this.listContainer.clear();
|
|
40
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
41
|
+
const isSelected = i === this.selectedIndex;
|
|
42
|
+
const text = isSelected
|
|
43
|
+
? theme.fg("accent", "→ ") + theme.fg("accent", this.options[i])
|
|
44
|
+
: ` ${theme.fg("text", this.options[i])}`;
|
|
45
|
+
this.listContainer.addChild(new Text(text, 1, 0));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
handleInput(keyData) {
|
|
49
|
+
const kb = getEditorKeybindings();
|
|
50
|
+
if (kb.matches(keyData, "selectUp") || keyData === "k") {
|
|
51
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
52
|
+
this.updateList();
|
|
53
|
+
}
|
|
54
|
+
else if (kb.matches(keyData, "selectDown") || keyData === "j") {
|
|
55
|
+
this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);
|
|
56
|
+
this.updateList();
|
|
57
|
+
}
|
|
58
|
+
else if (kb.matches(keyData, "selectConfirm") || keyData === "\n") {
|
|
59
|
+
const selected = this.options[this.selectedIndex];
|
|
60
|
+
if (selected)
|
|
61
|
+
this.onSelectCallback(selected);
|
|
62
|
+
}
|
|
63
|
+
else if (kb.matches(keyData, "selectCancel")) {
|
|
64
|
+
this.onCancelCallback();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
dispose() {
|
|
68
|
+
this.countdown?.dispose();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "indusagi/tui";
|
|
2
|
+
import { theme } from "../theme/theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Sanitize text for display in a single-line status.
|
|
5
|
+
* Removes newlines, tabs, carriage returns, and other control characters.
|
|
6
|
+
*/
|
|
7
|
+
function sanitizeStatusText(text) {
|
|
8
|
+
// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces
|
|
9
|
+
return text
|
|
10
|
+
.replace(/[\r\n\t]/g, " ")
|
|
11
|
+
.replace(/ +/g, " ")
|
|
12
|
+
.trim();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Format token counts (similar to web-ui)
|
|
16
|
+
*/
|
|
17
|
+
function formatTokens(count) {
|
|
18
|
+
if (count < 1000)
|
|
19
|
+
return count.toString();
|
|
20
|
+
if (count < 10000)
|
|
21
|
+
return `${(count / 1000).toFixed(1)}k`;
|
|
22
|
+
if (count < 1000000)
|
|
23
|
+
return `${Math.round(count / 1000)}k`;
|
|
24
|
+
if (count < 10000000)
|
|
25
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
26
|
+
return `${Math.round(count / 1000000)}M`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Footer component that shows pwd, token stats, and context usage.
|
|
30
|
+
* Computes token/context stats from session, gets git branch and extension statuses from provider.
|
|
31
|
+
*/
|
|
32
|
+
export class FooterComponent {
|
|
33
|
+
constructor(session, footerData) {
|
|
34
|
+
this.session = session;
|
|
35
|
+
this.footerData = footerData;
|
|
36
|
+
this.autoCompactEnabled = true;
|
|
37
|
+
}
|
|
38
|
+
setAutoCompactEnabled(enabled) {
|
|
39
|
+
this.autoCompactEnabled = enabled;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* No-op: git branch caching now handled by provider.
|
|
43
|
+
* Kept for compatibility with existing call sites in interactive-mode.
|
|
44
|
+
*/
|
|
45
|
+
invalidate() {
|
|
46
|
+
// No-op: git branch is cached/invalidated by provider
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Clean up resources.
|
|
50
|
+
* Git watcher cleanup now handled by provider.
|
|
51
|
+
*/
|
|
52
|
+
dispose() {
|
|
53
|
+
// Git watcher cleanup handled by provider
|
|
54
|
+
}
|
|
55
|
+
render(width) {
|
|
56
|
+
const state = this.session.state;
|
|
57
|
+
// Calculate cumulative usage from ALL session entries (not just post-compaction messages)
|
|
58
|
+
let totalInput = 0;
|
|
59
|
+
let totalOutput = 0;
|
|
60
|
+
let totalCacheRead = 0;
|
|
61
|
+
let totalCacheWrite = 0;
|
|
62
|
+
let totalCost = 0;
|
|
63
|
+
for (const entry of this.session.sessionManager.getEntries()) {
|
|
64
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
65
|
+
totalInput += entry.message.usage.input;
|
|
66
|
+
totalOutput += entry.message.usage.output;
|
|
67
|
+
totalCacheRead += entry.message.usage.cacheRead;
|
|
68
|
+
totalCacheWrite += entry.message.usage.cacheWrite;
|
|
69
|
+
totalCost += entry.message.usage.cost.total;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Get last assistant message for context percentage calculation (skip aborted messages)
|
|
73
|
+
const lastAssistantMessage = state.messages
|
|
74
|
+
.slice()
|
|
75
|
+
.reverse()
|
|
76
|
+
.find((m) => m.role === "assistant" && m.stopReason !== "aborted");
|
|
77
|
+
// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)
|
|
78
|
+
const contextTokens = lastAssistantMessage
|
|
79
|
+
? lastAssistantMessage.usage.input +
|
|
80
|
+
lastAssistantMessage.usage.output +
|
|
81
|
+
lastAssistantMessage.usage.cacheRead +
|
|
82
|
+
lastAssistantMessage.usage.cacheWrite
|
|
83
|
+
: 0;
|
|
84
|
+
const contextWindow = state.model?.contextWindow || 0;
|
|
85
|
+
const contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
86
|
+
const contextPercent = contextPercentValue.toFixed(1);
|
|
87
|
+
// Replace home directory with ~
|
|
88
|
+
let pwd = process.cwd();
|
|
89
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
90
|
+
if (home && pwd.startsWith(home)) {
|
|
91
|
+
pwd = `~${pwd.slice(home.length)}`;
|
|
92
|
+
}
|
|
93
|
+
// Add git branch if available
|
|
94
|
+
const branch = this.footerData.getGitBranch();
|
|
95
|
+
if (branch) {
|
|
96
|
+
pwd = `${pwd} (${branch})`;
|
|
97
|
+
}
|
|
98
|
+
// Add session name if set
|
|
99
|
+
const sessionName = this.session.sessionManager.getSessionName();
|
|
100
|
+
if (sessionName) {
|
|
101
|
+
pwd = `${pwd} • ${sessionName}`;
|
|
102
|
+
}
|
|
103
|
+
// Truncate path if too long to fit width
|
|
104
|
+
if (pwd.length > width) {
|
|
105
|
+
const half = Math.floor(width / 2) - 2;
|
|
106
|
+
if (half > 0) {
|
|
107
|
+
const start = pwd.slice(0, half);
|
|
108
|
+
const end = pwd.slice(-(half - 1));
|
|
109
|
+
pwd = `${start}...${end}`;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
pwd = pwd.slice(0, Math.max(1, width));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Build stats line
|
|
116
|
+
const statsParts = [];
|
|
117
|
+
if (totalInput)
|
|
118
|
+
statsParts.push(`↑${formatTokens(totalInput)}`);
|
|
119
|
+
if (totalOutput)
|
|
120
|
+
statsParts.push(`↓${formatTokens(totalOutput)}`);
|
|
121
|
+
if (totalCacheRead)
|
|
122
|
+
statsParts.push(`R${formatTokens(totalCacheRead)}`);
|
|
123
|
+
if (totalCacheWrite)
|
|
124
|
+
statsParts.push(`W${formatTokens(totalCacheWrite)}`);
|
|
125
|
+
// Show cost with "(sub)" indicator if using OAuth subscription
|
|
126
|
+
const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
|
|
127
|
+
if (totalCost || usingSubscription) {
|
|
128
|
+
const costStr = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
|
|
129
|
+
statsParts.push(costStr);
|
|
130
|
+
}
|
|
131
|
+
// Colorize context percentage based on usage
|
|
132
|
+
let contextPercentStr;
|
|
133
|
+
const autoIndicator = this.autoCompactEnabled ? " (auto)" : "";
|
|
134
|
+
const contextPercentDisplay = `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;
|
|
135
|
+
if (contextPercentValue > 90) {
|
|
136
|
+
contextPercentStr = theme.fg("error", contextPercentDisplay);
|
|
137
|
+
}
|
|
138
|
+
else if (contextPercentValue > 70) {
|
|
139
|
+
contextPercentStr = theme.fg("warning", contextPercentDisplay);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
contextPercentStr = contextPercentDisplay;
|
|
143
|
+
}
|
|
144
|
+
statsParts.push(contextPercentStr);
|
|
145
|
+
let statsLeft = statsParts.join(" ");
|
|
146
|
+
// Add model name on the right side, plus thinking level if model supports it
|
|
147
|
+
const modelName = state.model?.id || "no-model";
|
|
148
|
+
// Add thinking level hint if model supports reasoning and thinking is enabled
|
|
149
|
+
let rightSide = modelName;
|
|
150
|
+
if (state.model?.reasoning) {
|
|
151
|
+
const thinkingLevel = state.thinkingLevel || "off";
|
|
152
|
+
if (thinkingLevel !== "off") {
|
|
153
|
+
rightSide = `${modelName} • ${thinkingLevel}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Prepend the provider in parenthesis to the right side if there's multiple providers
|
|
157
|
+
if (this.footerData.getAvailableProviderCount() > 1 && state.model) {
|
|
158
|
+
rightSide = `(${state.model.provider}) ${rightSide}`;
|
|
159
|
+
}
|
|
160
|
+
let statsLeftWidth = visibleWidth(statsLeft);
|
|
161
|
+
const rightSideWidth = visibleWidth(rightSide);
|
|
162
|
+
// If statsLeft is too wide, truncate it
|
|
163
|
+
if (statsLeftWidth > width) {
|
|
164
|
+
// Truncate statsLeft to fit width (no room for right side)
|
|
165
|
+
const plainStatsLeft = statsLeft.replace(/\x1b\[[0-9;]*m/g, "");
|
|
166
|
+
statsLeft = `${plainStatsLeft.substring(0, width - 3)}...`;
|
|
167
|
+
statsLeftWidth = visibleWidth(statsLeft);
|
|
168
|
+
}
|
|
169
|
+
// Calculate available space for padding (minimum 2 spaces between stats and model)
|
|
170
|
+
const minPadding = 2;
|
|
171
|
+
const totalNeeded = statsLeftWidth + minPadding + rightSideWidth;
|
|
172
|
+
let statsLine;
|
|
173
|
+
if (totalNeeded <= width) {
|
|
174
|
+
// Both fit - add padding to right-align model
|
|
175
|
+
const padding = " ".repeat(width - statsLeftWidth - rightSideWidth);
|
|
176
|
+
statsLine = statsLeft + padding + rightSide;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Need to truncate right side
|
|
180
|
+
const availableForRight = width - statsLeftWidth - minPadding;
|
|
181
|
+
if (availableForRight > 3) {
|
|
182
|
+
// Truncate to fit (strip ANSI codes for length calculation, then truncate raw string)
|
|
183
|
+
const plainRightSide = rightSide.replace(/\x1b\[[0-9;]*m/g, "");
|
|
184
|
+
const truncatedPlain = plainRightSide.substring(0, availableForRight);
|
|
185
|
+
// For simplicity, just use plain truncated version (loses color, but fits)
|
|
186
|
+
const padding = " ".repeat(width - statsLeftWidth - truncatedPlain.length);
|
|
187
|
+
statsLine = statsLeft + padding + truncatedPlain;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// Not enough space for right side at all
|
|
191
|
+
statsLine = statsLeft;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Apply dim to each part separately. statsLeft may contain color codes (for context %)
|
|
195
|
+
// that end with a reset, which would clear an outer dim wrapper. So we dim the parts
|
|
196
|
+
// before and after the colored section independently.
|
|
197
|
+
const dimStatsLeft = theme.fg("dim", statsLeft);
|
|
198
|
+
const remainder = statsLine.slice(statsLeft.length); // padding + rightSide
|
|
199
|
+
const dimRemainder = theme.fg("dim", remainder);
|
|
200
|
+
const lines = [theme.fg("dim", pwd), dimStatsLeft + dimRemainder];
|
|
201
|
+
// Add extension statuses on a single line, sorted by key alphabetically
|
|
202
|
+
const extensionStatuses = this.footerData.getExtensionStatuses();
|
|
203
|
+
if (extensionStatuses.size > 0) {
|
|
204
|
+
const sortedStatuses = Array.from(extensionStatuses.entries())
|
|
205
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
206
|
+
.map(([, text]) => sanitizeStatusText(text));
|
|
207
|
+
const statusLine = sortedStatuses.join(" ");
|
|
208
|
+
// Truncate to terminal width with dim ellipsis for consistency with footer style
|
|
209
|
+
lines.push(truncateToWidth(statusLine, width, theme.fg("dim", "...")));
|
|
210
|
+
}
|
|
211
|
+
return lines;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// UI Components for extensions
|
|
2
|
+
export { ArminComponent } from "./armin.js";
|
|
3
|
+
export { AssistantMessageComponent } from "./assistant-message.js";
|
|
4
|
+
export { BashExecutionComponent } from "./bash-execution.js";
|
|
5
|
+
export { BorderedLoader } from "./bordered-loader.js";
|
|
6
|
+
export { BranchSummaryMessageComponent } from "./branch-summary-message.js";
|
|
7
|
+
export { CompactionSummaryMessageComponent } from "./compaction-summary-message.js";
|
|
8
|
+
export { CustomEditor } from "./custom-editor.js";
|
|
9
|
+
export { CustomMessageComponent } from "./custom-message.js";
|
|
10
|
+
export { renderDiff } from "./diff.js";
|
|
11
|
+
export { DynamicBorder } from "./dynamic-border.js";
|
|
12
|
+
export { ExtensionEditorComponent } from "./extension-editor.js";
|
|
13
|
+
export { ExtensionInputComponent } from "./extension-input.js";
|
|
14
|
+
export { ExtensionSelectorComponent } from "./extension-selector.js";
|
|
15
|
+
export { FooterComponent } from "./footer.js";
|
|
16
|
+
export { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./keybinding-hints.js";
|
|
17
|
+
export { LoginDialogComponent } from "./login-dialog.js";
|
|
18
|
+
export { ModelSelectorComponent } from "./model-selector.js";
|
|
19
|
+
export { OAuthSelectorComponent } from "./oauth-selector.js";
|
|
20
|
+
export { ScopedModelsSelectorComponent } from "./scoped-models-selector.js";
|
|
21
|
+
export { SessionSelectorComponent } from "./session-selector.js";
|
|
22
|
+
export { SettingsSelectorComponent } from "./settings-selector.js";
|
|
23
|
+
export { ShowImagesSelectorComponent } from "./show-images-selector.js";
|
|
24
|
+
export { SkillInvocationMessageComponent } from "./skill-invocation-message.js";
|
|
25
|
+
export { ThemeSelectorComponent } from "./theme-selector.js";
|
|
26
|
+
export { ThinkingSelectorComponent } from "./thinking-selector.js";
|
|
27
|
+
export { ToolExecutionComponent } from "./tool-execution.js";
|
|
28
|
+
export { TreeSelectorComponent } from "./tree-selector.js";
|
|
29
|
+
export { UserMessageComponent } from "./user-message.js";
|
|
30
|
+
export { UserMessageSelectorComponent } from "./user-message-selector.js";
|
|
31
|
+
export { truncateToVisualLines } from "./visual-truncate.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for formatting keybinding hints in the UI.
|
|
3
|
+
*/
|
|
4
|
+
import { getEditorKeybindings } from "indusagi/tui";
|
|
5
|
+
import { theme } from "../theme/theme.js";
|
|
6
|
+
/**
|
|
7
|
+
* Format keys array as display string (e.g., ["ctrl+c", "escape"] -> "ctrl+c/escape").
|
|
8
|
+
*/
|
|
9
|
+
function formatKeys(keys) {
|
|
10
|
+
if (keys.length === 0)
|
|
11
|
+
return "";
|
|
12
|
+
if (keys.length === 1)
|
|
13
|
+
return keys[0];
|
|
14
|
+
return keys.join("/");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get display string for an editor action.
|
|
18
|
+
*/
|
|
19
|
+
export function editorKey(action) {
|
|
20
|
+
return formatKeys(getEditorKeybindings().getKeys(action));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get display string for an app action.
|
|
24
|
+
*/
|
|
25
|
+
export function appKey(keybindings, action) {
|
|
26
|
+
return formatKeys(keybindings.getKeys(action));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Format a keybinding hint with consistent styling: dim key, muted description.
|
|
30
|
+
* Looks up the key from editor keybindings automatically.
|
|
31
|
+
*
|
|
32
|
+
* @param action - Editor action name (e.g., "selectConfirm", "expandTools")
|
|
33
|
+
* @param description - Description text (e.g., "to expand", "cancel")
|
|
34
|
+
* @returns Formatted string with dim key and muted description
|
|
35
|
+
*/
|
|
36
|
+
export function keyHint(action, description) {
|
|
37
|
+
return theme.fg("dim", editorKey(action)) + theme.fg("muted", ` ${description}`);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Format a keybinding hint for app-level actions.
|
|
41
|
+
* Requires the KeybindingsManager instance.
|
|
42
|
+
*
|
|
43
|
+
* @param keybindings - KeybindingsManager instance
|
|
44
|
+
* @param action - App action name (e.g., "interrupt", "externalEditor")
|
|
45
|
+
* @param description - Description text
|
|
46
|
+
* @returns Formatted string with dim key and muted description
|
|
47
|
+
*/
|
|
48
|
+
export function appKeyHint(keybindings, action, description) {
|
|
49
|
+
return theme.fg("dim", appKey(keybindings, action)) + theme.fg("muted", ` ${description}`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Format a raw key string with description (for non-configurable keys like ↑↓).
|
|
53
|
+
*
|
|
54
|
+
* @param key - Raw key string
|
|
55
|
+
* @param description - Description text
|
|
56
|
+
* @returns Formatted string with dim key and muted description
|
|
57
|
+
*/
|
|
58
|
+
export function rawKeyHint(key, description) {
|
|
59
|
+
return theme.fg("dim", key) + theme.fg("muted", ` ${description}`);
|
|
60
|
+
}
|