@superjackfruitlabs/salaam 0.0.4 → 0.0.5
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 +41 -0
- package/dist/src/acp-desk.js +140 -0
- package/dist/src/acp-outcome.js +23 -19
- package/dist/src/acp.js +258 -11
- package/dist/src/approval-rules.js +287 -0
- package/dist/src/attach.js +3 -1
- package/dist/src/autonomy.js +80 -9
- package/dist/src/cli-help.js +34 -4
- package/dist/src/cli.js +55 -3
- package/dist/src/command-danger.js +797 -0
- package/dist/src/command-sandbox.js +116 -6
- package/dist/src/custom-commands.js +159 -0
- package/dist/src/danger-classifier.js +286 -0
- package/dist/src/git-helpers.js +160 -0
- package/dist/src/hooks.js +351 -0
- package/dist/src/host.js +96 -4
- package/dist/src/house-extension.js +133 -7
- package/dist/src/local-coding.js +553 -58
- package/dist/src/local-edits.js +168 -6
- package/dist/src/mentions.js +277 -0
- package/dist/src/pi-sessions.js +5 -1
- package/dist/src/project-instructions.js +195 -0
- package/dist/src/protocol.js +4 -1
- package/dist/src/service.js +342 -5
- package/dist/src/session-pick.js +77 -0
- package/dist/src/shell-passthrough.js +39 -0
- package/dist/src/store.js +51 -4
- package/dist/src/supervisor.js +233 -22
- package/dist/src/task-context.js +8 -2
- package/dist/src/terminal-status.js +88 -0
- package/dist/src/tui-activity.js +99 -0
- package/dist/src/tui-classifier.js +25 -0
- package/dist/src/tui-client.js +186 -5
- package/dist/src/tui-commands.js +23 -5
- package/dist/src/tui-composer.js +113 -0
- package/dist/src/tui-keymap.js +116 -0
- package/dist/src/tui-mentions.js +38 -0
- package/dist/src/tui-modals.js +157 -34
- package/dist/src/tui-notify.js +62 -0
- package/dist/src/tui-review.js +55 -0
- package/dist/src/tui-turn-keys.js +52 -0
- package/dist/src/tui-view.js +243 -69
- package/dist/src/tui.js +875 -118
- package/dist/src/turn-checkpoints.js +221 -0
- package/dist/src/turn-rewind.js +168 -0
- package/dist/src/web-fetch.js +114 -0
- package/dist/src/web-guard.js +163 -0
- package/dist/src/web-search.js +302 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/acp-desk.ts +155 -0
- package/src/acp-outcome.ts +25 -21
- package/src/acp.ts +338 -28
- package/src/approval-rules.ts +360 -0
- package/src/attach.ts +4 -1
- package/src/autonomy.ts +111 -10
- package/src/cli-help.ts +33 -4
- package/src/cli.ts +66 -2
- package/src/command-danger.ts +886 -0
- package/src/command-sandbox.ts +128 -8
- package/src/custom-commands.ts +175 -0
- package/src/danger-classifier.ts +321 -0
- package/src/git-helpers.ts +177 -0
- package/src/hooks.ts +425 -0
- package/src/host.ts +122 -1
- package/src/house-extension.ts +142 -7
- package/src/local-coding.ts +674 -75
- package/src/local-edits.ts +191 -8
- package/src/mentions.ts +304 -0
- package/src/pi-sessions.ts +9 -1
- package/src/project-instructions.ts +229 -0
- package/src/protocol.ts +19 -2
- package/src/service.ts +488 -5
- package/src/session-pick.ts +102 -0
- package/src/shell-passthrough.ts +38 -0
- package/src/store.ts +52 -3
- package/src/supervisor.ts +262 -24
- package/src/task-context.ts +9 -2
- package/src/terminal-status.ts +103 -0
- package/src/tui-activity.ts +133 -0
- package/src/tui-classifier.ts +38 -0
- package/src/tui-client.ts +239 -8
- package/src/tui-commands.ts +49 -10
- package/src/tui-composer.ts +124 -0
- package/src/tui-keymap.ts +152 -0
- package/src/tui-mentions.ts +43 -0
- package/src/tui-modals.ts +173 -43
- package/src/tui-notify.ts +77 -0
- package/src/tui-review.ts +76 -0
- package/src/tui-turn-keys.ts +77 -0
- package/src/tui-view.ts +341 -88
- package/src/tui.ts +1047 -137
- package/src/turn-checkpoints.ts +253 -0
- package/src/turn-rewind.ts +258 -0
- package/src/web-fetch.ts +140 -0
- package/src/web-guard.ts +195 -0
- package/src/web-search.ts +402 -0
package/src/tui-commands.ts
CHANGED
|
@@ -17,15 +17,11 @@ export const SLASH_COMMANDS: readonly SlashDefinition[] = [
|
|
|
17
17
|
command('help', 'Show commands and their execution type', ['commands'], '[command]'),
|
|
18
18
|
command('exit', 'Detach; preserve the session and running work', ['quit', 'q']),
|
|
19
19
|
command('keys', 'Show keyboard shortcuts', ['hotkeys']),
|
|
20
|
-
command('model', '
|
|
21
|
-
command('provider', '
|
|
22
|
-
command('thinking', '
|
|
23
|
-
command(
|
|
24
|
-
|
|
25
|
-
'New local coding session with reviewed edits and unsandboxed commands',
|
|
26
|
-
[],
|
|
27
|
-
'[workspace]',
|
|
28
|
-
),
|
|
20
|
+
command('model', 'Switch this session’s provider and model', ['models'], '[provider/model]'),
|
|
21
|
+
command('provider', 'Switch this session’s provider', [], '[provider]'),
|
|
22
|
+
command('thinking', 'Switch this session’s reasoning effort', [], '[level]'),
|
|
23
|
+
command('coding', 'New local coding session with reviewed edits and commands', [], '[workspace]'),
|
|
24
|
+
command('init', 'Draft an AGENTS.md for this repository; the edit asks for approval'),
|
|
29
25
|
command('new', 'Create a session in a workspace', [], '[workspace]'),
|
|
30
26
|
command('sessions', 'Switch or resume a session', ['resume'], '[session-id]'),
|
|
31
27
|
command('name', 'Set a durable session label', ['rename'], '<name>'),
|
|
@@ -39,9 +35,30 @@ export const SLASH_COMMANDS: readonly SlashDefinition[] = [
|
|
|
39
35
|
[],
|
|
40
36
|
'<text>',
|
|
41
37
|
),
|
|
42
|
-
command('diff', 'Review workspace changes'),
|
|
38
|
+
command('diff', 'Review workspace changes file by file'),
|
|
39
|
+
command(
|
|
40
|
+
'commit',
|
|
41
|
+
'Draft a commit of the current work; staging and the commit each ask (uses provider credits)',
|
|
42
|
+
[],
|
|
43
|
+
undefined,
|
|
44
|
+
'model',
|
|
45
|
+
),
|
|
46
|
+
command('branch', 'Switch to a branch, creating it if needed', [], '<name>', 'model'),
|
|
47
|
+
command(
|
|
48
|
+
'pr',
|
|
49
|
+
'Push this branch and open a pull request; both always ask, except in YOLO',
|
|
50
|
+
[],
|
|
51
|
+
'[base]',
|
|
52
|
+
'model',
|
|
53
|
+
),
|
|
43
54
|
command('tools', 'Inspect available capabilities and recorded tool activity'),
|
|
44
55
|
command('permissions', 'Configure local permissions independently of task progression'),
|
|
56
|
+
command('mode', 'Cycle review → accept edits → auto (shift+tab)'),
|
|
57
|
+
command(
|
|
58
|
+
'full-access',
|
|
59
|
+
'Turn full access on or off: commands run without the OS sandbox; dangerous ones still ask',
|
|
60
|
+
),
|
|
61
|
+
command('yolo', 'Turn YOLO on (type yolo to confirm) or off: no sandbox and no approvals'),
|
|
45
62
|
command('approvals', 'Review exact pending approval requests'),
|
|
46
63
|
command('context', 'Inspect session context, facts and usage'),
|
|
47
64
|
command('usage', 'Show recorded usage and unknown observations'),
|
|
@@ -50,6 +67,7 @@ export const SLASH_COMMANDS: readonly SlashDefinition[] = [
|
|
|
50
67
|
command('export', 'Export full durable conversation as Markdown; never overwrite', [], '[path]'),
|
|
51
68
|
command('settings', 'Choose terminal appearance and model settings'),
|
|
52
69
|
command('fork', 'Fork an idle local conversation; keep the same workspace'),
|
|
70
|
+
command('rewind', 'Rewind files, conversation or both to before an earlier turn', [], '[turn]'),
|
|
53
71
|
command(
|
|
54
72
|
'undo',
|
|
55
73
|
'Review and revert the last completed local edit; retain conversation history',
|
|
@@ -80,6 +98,19 @@ export const SLASH_COMMANDS: readonly SlashDefinition[] = [
|
|
|
80
98
|
command('agents', 'Inspect native/external worker profiles and child work'),
|
|
81
99
|
command('progression', 'Choose manual or bounded task progression; permissions stay separate'),
|
|
82
100
|
command('tasks', 'Open durable task actions'),
|
|
101
|
+
command(
|
|
102
|
+
'classifier',
|
|
103
|
+
'Pick the small-model command reviewer for this session, or turn it off',
|
|
104
|
+
[],
|
|
105
|
+
'[name|on|off]',
|
|
106
|
+
),
|
|
107
|
+
command(
|
|
108
|
+
'hooks',
|
|
109
|
+
'Show your hooks and this workspace’s; trust or revoke the workspace file',
|
|
110
|
+
[],
|
|
111
|
+
'[trust|revoke]',
|
|
112
|
+
),
|
|
113
|
+
command('jobs', 'Show background commands and their output, or stop one', ['bg'], '[stop <id>]'),
|
|
83
114
|
];
|
|
84
115
|
export const SLASH_COMPLETIONS = SLASH_COMMANDS.flatMap((c) =>
|
|
85
116
|
[c.name, ...c.aliases].map((name) => ({ name, description: c.description })),
|
|
@@ -125,3 +156,11 @@ export function slashHelp(name?: string): string {
|
|
|
125
156
|
)
|
|
126
157
|
.join('\n');
|
|
127
158
|
}
|
|
159
|
+
|
|
160
|
+
/** What /init asks the agent to do. The write goes through the session's normal edit approval. */
|
|
161
|
+
export const INIT_INSTRUCTION = [
|
|
162
|
+
'Draft an AGENTS.md for this repository.',
|
|
163
|
+
'First read the tree and the files that define how it is built and tested (package manifests, build and CI configuration, README, existing contributor docs, any CLAUDE.md).',
|
|
164
|
+
'Then write AGENTS.md at the repository root: a short overview, the exact commands to build, test, lint and format, the code layout, and conventions a contributor must follow.',
|
|
165
|
+
'Keep it under 150 lines, state only what the files show, and if AGENTS.md already exists, propose improvements to it instead of replacing it.',
|
|
166
|
+
].join(' ');
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Composer behaviour for the trial-polish batch (operator approval 2026-10-31): Ctrl+C in steps,
|
|
7
|
+
* reverse history search, persisted history that never keeps secret-looking lines, and `!command`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** What Ctrl+C does now: stop a running turn, else clear typed text, else arm, else leave. */
|
|
11
|
+
export type CtrlCAction = 'interrupt' | 'clear' | 'arm' | 'exit';
|
|
12
|
+
export const CTRL_C_WINDOW_MS = 2000;
|
|
13
|
+
|
|
14
|
+
export function ctrlCAction(input: {
|
|
15
|
+
running: boolean;
|
|
16
|
+
text: string;
|
|
17
|
+
armedAt: number | undefined;
|
|
18
|
+
now: number;
|
|
19
|
+
}): CtrlCAction {
|
|
20
|
+
if (input.running) return 'interrupt';
|
|
21
|
+
if (input.text !== '') return 'clear';
|
|
22
|
+
if (input.armedAt !== undefined && input.now - input.armedAt <= CTRL_C_WINDOW_MS) return 'exit';
|
|
23
|
+
return 'arm';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SECRET_PATTERNS: RegExp[] = [
|
|
27
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
28
|
+
/\b(sk|rk|pk)-[A-Za-z0-9_-]{16,}/,
|
|
29
|
+
/\bsk-ant-[A-Za-z0-9_-]{8,}/,
|
|
30
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}/,
|
|
31
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}/,
|
|
32
|
+
/\bxox[abprs]-[A-Za-z0-9-]{10,}/,
|
|
33
|
+
/\bAKIA[0-9A-Z]{16}\b/,
|
|
34
|
+
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\./,
|
|
35
|
+
/\b(password|passwd|secret|token|api[_-]?key|authorization)\s*[:=]\s*\S{6,}/i,
|
|
36
|
+
/\bbearer\s+[A-Za-z0-9._~+/-]{16,}/i,
|
|
37
|
+
/[A-Za-z0-9+_-]{48,}={0,2}/,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/** True when the text looks like it carries a credential; such text is never written to history. */
|
|
41
|
+
export function looksSecret(text: string): boolean {
|
|
42
|
+
return SECRET_PATTERNS.some((p) => p.test(text));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const HISTORY_LIMIT = 500;
|
|
46
|
+
export const HISTORY_ENTRY_LIMIT = 8192;
|
|
47
|
+
|
|
48
|
+
/** Where this user's composer history lives: $XDG_STATE_HOME/salaam/history.json, or nowhere when
|
|
49
|
+
* SALAAM_HISTORY=off (history then lasts only for this terminal). */
|
|
50
|
+
export function historyPath(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
51
|
+
if (env.SALAAM_HISTORY === 'off') return undefined;
|
|
52
|
+
const base = env.XDG_STATE_HOME || join(env.HOME || homedir(), '.local', 'state');
|
|
53
|
+
return join(base, 'salaam', 'history.json');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Per-user composer history, newest last, bounded, and without secret-looking entries. */
|
|
57
|
+
export class ComposerHistory {
|
|
58
|
+
entries: string[] = [];
|
|
59
|
+
constructor(private readonly path: string | undefined) {
|
|
60
|
+
try {
|
|
61
|
+
if (path && existsSync(path)) {
|
|
62
|
+
const raw: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
63
|
+
if (Array.isArray(raw))
|
|
64
|
+
this.entries = raw
|
|
65
|
+
.filter((e): e is string => typeof e === 'string')
|
|
66
|
+
.filter((e) => keepable(e))
|
|
67
|
+
.slice(-HISTORY_LIMIT);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
this.entries = [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Adds a sent message. Returns false when it was not kept (blank, too long, or secret-looking). */
|
|
74
|
+
add(text: string): boolean {
|
|
75
|
+
if (!keepable(text)) return false;
|
|
76
|
+
this.entries = this.entries.filter((e) => e !== text);
|
|
77
|
+
this.entries.push(text);
|
|
78
|
+
if (this.entries.length > HISTORY_LIMIT) this.entries = this.entries.slice(-HISTORY_LIMIT);
|
|
79
|
+
this.save();
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
private save(): void {
|
|
83
|
+
if (!this.path) return;
|
|
84
|
+
try {
|
|
85
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
86
|
+
const tmp = this.path + '.tmp';
|
|
87
|
+
writeFileSync(tmp, JSON.stringify(this.entries), { mode: 0o600 });
|
|
88
|
+
chmodSync(tmp, 0o600);
|
|
89
|
+
renameSync(tmp, this.path);
|
|
90
|
+
} catch {
|
|
91
|
+
// History is a convenience; failing to save never blocks sending.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function keepable(text: string): boolean {
|
|
97
|
+
return text.trim() !== '' && text.length <= HISTORY_ENTRY_LIMIT && !looksSecret(text);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reverse-incremental search: the newest entry older than `before` (an index into `entries`,
|
|
102
|
+
* newest last) that contains the query, case-insensitively. Returns its index or -1.
|
|
103
|
+
*/
|
|
104
|
+
export function searchHistory(entries: string[], query: string, before: number): number {
|
|
105
|
+
const q = query.toLowerCase();
|
|
106
|
+
for (let i = Math.min(before, entries.length) - 1; i >= 0; i--)
|
|
107
|
+
if (entries[i]!.toLowerCase().includes(q)) return i;
|
|
108
|
+
return -1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The status-line text while searching, matching readline's wording. */
|
|
112
|
+
export function searchPrompt(query: string, match: string | undefined): string {
|
|
113
|
+
const shown = match === undefined ? '' : match.replace(/\s+/g, ' ').slice(0, 120);
|
|
114
|
+
return match === undefined && query !== ''
|
|
115
|
+
? `(failing reverse-i-search)\`${query}': · esc cancels`
|
|
116
|
+
: `(reverse-i-search)\`${query}': ${shown} · enter keeps · ctrl+r older · esc cancels`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* `!command` from the composer. The TUI never runs it itself: it becomes an instruction for the
|
|
121
|
+
* model to run exactly this command through salaam_local, so it takes the same approval path,
|
|
122
|
+
* session mode, sandbox and danger check as any model command. Returns undefined for other text.
|
|
123
|
+
*/
|
|
124
|
+
export { shellPassthrough } from './shell-passthrough.js';
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { KeyId } from '@earendil-works/pi-tui';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The TUI's keys in one table (trial polish C, 2026-10-31). The global bindings in tui.ts and the
|
|
5
|
+
* `?` key sheet both read this, so the sheet cannot drift from what the keys do.
|
|
6
|
+
*/
|
|
7
|
+
export type GlobalAction =
|
|
8
|
+
| 'palette'
|
|
9
|
+
| 'sessions'
|
|
10
|
+
| 'new'
|
|
11
|
+
| 'approvals'
|
|
12
|
+
| 'changes'
|
|
13
|
+
| 'commands'
|
|
14
|
+
| 'rail'
|
|
15
|
+
| 'transcript'
|
|
16
|
+
| 'refresh'
|
|
17
|
+
| 'editor'
|
|
18
|
+
| 'mode'
|
|
19
|
+
| 'help';
|
|
20
|
+
|
|
21
|
+
export const GLOBAL_KEYS: readonly { key: KeyId; action: GlobalAction; label: string }[] = [
|
|
22
|
+
{ key: 'ctrl+k', action: 'palette', label: 'all actions' },
|
|
23
|
+
{ key: 'f2', action: 'palette', label: 'all actions' },
|
|
24
|
+
{ key: 'ctrl+p', action: 'sessions', label: 'sessions' },
|
|
25
|
+
{ key: 'ctrl+n', action: 'new', label: 'new session' },
|
|
26
|
+
{ key: 'ctrl+o', action: 'approvals', label: 'approvals' },
|
|
27
|
+
{ key: 'alt+g', action: 'changes', label: 'changes' },
|
|
28
|
+
{ key: 'ctrl+t', action: 'commands', label: 'commands' },
|
|
29
|
+
{ key: 'ctrl+b', action: 'rail', label: 'show or hide the side panel' },
|
|
30
|
+
{ key: 'ctrl+x', action: 'transcript', label: 'full transcript on or off' },
|
|
31
|
+
{ key: 'f5', action: 'refresh', label: 'refresh' },
|
|
32
|
+
{ key: 'ctrl+g', action: 'editor', label: 'external prompt editor' },
|
|
33
|
+
{ key: 'shift+tab', action: 'mode', label: 'next mode' },
|
|
34
|
+
{ key: 'f1', action: 'help', label: 'these keys' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** The per-file review on the Changes tab of a desk session. */
|
|
38
|
+
export const REVIEW_KEYS = {
|
|
39
|
+
nextFile: { key: 'alt+right', label: 'next file' },
|
|
40
|
+
previousFile: { key: 'alt+left', label: 'previous file' },
|
|
41
|
+
nextHunk: { key: 'alt+down', label: 'next hunk' },
|
|
42
|
+
previousHunk: { key: 'alt+up', label: 'previous hunk' },
|
|
43
|
+
accept: { key: 'a', label: 'accept the file or hunk' },
|
|
44
|
+
revert: { key: 'r', label: 'revert it (asks first)' },
|
|
45
|
+
instruct: { key: 'i', label: 'tell the model about it' },
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
/** Keys while a desk turn runs (tui-turn-keys.ts decides them). */
|
|
49
|
+
export const TURN_KEYS = [
|
|
50
|
+
{ key: 'escape', label: 'stop the turn' },
|
|
51
|
+
{ key: 'enter', label: 'steer it' },
|
|
52
|
+
{ key: 'tab', label: 'queue for after' },
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
const COMPOSER: { label: string; description?: string }[] = [
|
|
56
|
+
{ label: 'Enter · send', description: 'The durable outbox saves it before delivery' },
|
|
57
|
+
{ label: 'Alt+Enter / Ctrl+J · new line' },
|
|
58
|
+
{ label: 'Tab · switch view', description: 'Conversation · Changes · Commands · Context' },
|
|
59
|
+
{ label: 'PageUp / PageDown · scroll', description: 'Ctrl+Home/End jump to the top/bottom' },
|
|
60
|
+
{ label: '@ · attach a file or image' },
|
|
61
|
+
{ label: '/ · slash commands' },
|
|
62
|
+
{
|
|
63
|
+
label: 'Ctrl+R · search sent messages',
|
|
64
|
+
description: 'Ctrl+R for older, Enter keeps, Esc restores',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: 'Ctrl+A/E · Ctrl+U/K/W · Ctrl+Y · Ctrl+-',
|
|
68
|
+
description: 'Line start/end, kill to start/end/word, yank, undo; Alt+B/F words',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: '!command · run a shell command',
|
|
72
|
+
description: 'Same approval, sandbox and danger check',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: 'Ctrl+C · stop, clear, then leave',
|
|
76
|
+
description: 'Stops a running turn, else clears text; twice on empty leaves. Work continues.',
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
const CODING_COMMANDS = [
|
|
80
|
+
{ label: '/rewind · go back to an earlier turn' },
|
|
81
|
+
{ label: '/jobs · background commands' },
|
|
82
|
+
{ label: '/undo · revert the last edit' },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
export function keyLabel(key: string): string {
|
|
86
|
+
const names: Record<string, string> = { escape: 'Esc', enter: 'Enter', tab: 'Tab' };
|
|
87
|
+
if (names[key]) return names[key];
|
|
88
|
+
if (key.length === 1) return key;
|
|
89
|
+
return key
|
|
90
|
+
.split('+')
|
|
91
|
+
.map((part) =>
|
|
92
|
+
part === 'right'
|
|
93
|
+
? '→'
|
|
94
|
+
: part === 'left'
|
|
95
|
+
? '←'
|
|
96
|
+
: part === 'up'
|
|
97
|
+
? '↑'
|
|
98
|
+
: part === 'down'
|
|
99
|
+
? '↓'
|
|
100
|
+
: part.length <= 2
|
|
101
|
+
? part.toUpperCase()
|
|
102
|
+
: part[0]!.toUpperCase() + part.slice(1),
|
|
103
|
+
)
|
|
104
|
+
.join('+');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface KeyContext {
|
|
108
|
+
tab: string;
|
|
109
|
+
coding: boolean;
|
|
110
|
+
running: boolean;
|
|
111
|
+
/** Your own slash commands (Phase 4b), listed in the conversation sheet. */
|
|
112
|
+
custom?: readonly { name: string; description: string; argumentHint?: string }[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function keySheet(c: KeyContext): {
|
|
116
|
+
title: string;
|
|
117
|
+
rows: { label: string; description?: string }[];
|
|
118
|
+
} {
|
|
119
|
+
const global = GLOBAL_KEYS.map((k) => ({ label: keyLabel(k.key) + ' · ' + k.label }));
|
|
120
|
+
const approvals = { label: 'Approvals show their own keys along the bottom' };
|
|
121
|
+
if (c.tab === 'changes' && c.coding)
|
|
122
|
+
return {
|
|
123
|
+
title: 'Keys · Review changes',
|
|
124
|
+
rows: [
|
|
125
|
+
...Object.values(REVIEW_KEYS).map((k) => ({ label: keyLabel(k.key) + ' · ' + k.label })),
|
|
126
|
+
{ label: 'a, r and i work while the composer is empty' },
|
|
127
|
+
...global,
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
if (c.coding && c.running)
|
|
131
|
+
return {
|
|
132
|
+
title: 'Keys · Turn running',
|
|
133
|
+
rows: [
|
|
134
|
+
...TURN_KEYS.map((k) => ({ label: keyLabel(k.key) + ' · ' + k.label })),
|
|
135
|
+
...global,
|
|
136
|
+
approvals,
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
title: 'Keys · Conversation',
|
|
141
|
+
rows: [
|
|
142
|
+
...COMPOSER,
|
|
143
|
+
...(c.coding ? CODING_COMMANDS : []),
|
|
144
|
+
...(c.custom ?? []).map((k) => ({
|
|
145
|
+
label: `/${k.name}${k.argumentHint ? ' ' + k.argumentHint : ''} · your command`,
|
|
146
|
+
description: k.description,
|
|
147
|
+
})),
|
|
148
|
+
...global,
|
|
149
|
+
approvals,
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CombinedAutocompleteProvider } from '@earendil-works/pi-tui';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { listWorkspaceFiles, rankFiles } from './mentions.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `@` completes workspace files from git's own listing (ignored files stay out) instead of
|
|
7
|
+
* pi-tui's `fd` walk, which is absent on most desks. The listing is cached for 5 s.
|
|
8
|
+
*/
|
|
9
|
+
export class MentionAutocompleteProvider extends CombinedAutocompleteProvider {
|
|
10
|
+
private cache?: { at: number; files: string[] };
|
|
11
|
+
constructor(
|
|
12
|
+
commands: ConstructorParameters<typeof CombinedAutocompleteProvider>[0],
|
|
13
|
+
private readonly root: string,
|
|
14
|
+
) {
|
|
15
|
+
super(commands, root, null);
|
|
16
|
+
}
|
|
17
|
+
files(): string[] {
|
|
18
|
+
if (!this.cache || Date.now() - this.cache.at > 5000)
|
|
19
|
+
this.cache = { at: Date.now(), files: listWorkspaceFiles(this.root) };
|
|
20
|
+
return this.cache.files;
|
|
21
|
+
}
|
|
22
|
+
override async getSuggestions(
|
|
23
|
+
lines: string[],
|
|
24
|
+
cursorLine: number,
|
|
25
|
+
cursorCol: number,
|
|
26
|
+
options: { signal: AbortSignal; force?: boolean },
|
|
27
|
+
): ReturnType<CombinedAutocompleteProvider['getSuggestions']> {
|
|
28
|
+
const before = (lines[cursorLine] ?? '').slice(0, cursorCol);
|
|
29
|
+
const match = /(?:^|[\s([{])(@"[^"]*|@[^\s"]*)$/.exec(before);
|
|
30
|
+
if (!match) return super.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
31
|
+
if (options.signal.aborted) return null;
|
|
32
|
+
const prefix = match[1]!;
|
|
33
|
+
const quoted = prefix.startsWith('@"');
|
|
34
|
+
const items = rankFiles(this.files(), prefix.slice(quoted ? 2 : 1))
|
|
35
|
+
.slice(0, 20)
|
|
36
|
+
.map((file) => ({
|
|
37
|
+
value: quoted || /\s/.test(file) ? `@"${file}"` : `@${file}`,
|
|
38
|
+
label: basename(file),
|
|
39
|
+
description: file,
|
|
40
|
+
}));
|
|
41
|
+
return items.length ? { items, prefix } : null;
|
|
42
|
+
}
|
|
43
|
+
}
|