@aragon-agent/cli 0.6.24 → 0.6.26
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 +1924 -1786
- package/README.md +2730 -2562
- package/dist/agent/controller.d.ts +49 -3
- package/dist/agent/controller.js +78 -3
- package/dist/agent/conversation-reset.d.ts +91 -0
- package/dist/agent/conversation-reset.js +66 -0
- package/dist/agent/model-probe-limits.d.ts +39 -0
- package/dist/agent/model-probe-limits.js +25 -0
- package/dist/agent/model-probe.d.ts +104 -0
- package/dist/agent/model-probe.js +391 -0
- package/dist/agent/reducer.d.ts +22 -0
- package/dist/agent/reducer.js +49 -14
- package/dist/cli.js +214 -2
- package/dist/commands/branch-command.d.ts +61 -0
- package/dist/commands/branch-command.js +117 -0
- package/dist/commands/builtins.js +62 -40
- package/dist/commands/clear-command.d.ts +27 -0
- package/dist/commands/clear-command.js +107 -0
- package/dist/commands/registry.d.ts +48 -1
- package/dist/commands/session-command.d.ts +54 -0
- package/dist/commands/session-command.js +109 -0
- package/dist/config/load.d.ts +15 -0
- package/dist/config/model-profiles.d.ts +8 -0
- package/dist/config/model-profiles.js +10 -0
- package/dist/exec/index.js +10 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/terminal-plan.d.ts +155 -0
- package/dist/proc/terminal-plan.js +360 -0
- package/dist/session/branch-invocation.d.ts +83 -0
- package/dist/session/branch-invocation.js +109 -0
- package/dist/session/branch-launch.d.ts +70 -0
- package/dist/session/branch-launch.js +248 -0
- package/dist/session/checkpoint.d.ts +257 -0
- package/dist/session/checkpoint.js +512 -0
- package/dist/session/clear-archive.d.ts +51 -0
- package/dist/session/clear-archive.js +160 -0
- package/dist/session/cli-commands.d.ts +2 -0
- package/dist/session/cli-commands.js +36 -5
- package/dist/session/identity.d.ts +72 -0
- package/dist/session/identity.js +148 -0
- package/dist/session/limits.d.ts +66 -0
- package/dist/session/limits.js +66 -0
- package/dist/session/persist.d.ts +30 -5
- package/dist/session/startup-session.d.ts +107 -0
- package/dist/session/startup-session.js +315 -0
- package/dist/session/store.d.ts +60 -1
- package/dist/session/store.js +95 -6
- package/dist/todo/types.d.ts +5 -3
- package/dist/ui/App.d.ts +54 -2
- package/dist/ui/App.js +154 -28
- package/dist/ui/Header.js +8 -16
- package/dist/ui/StatusBar.d.ts +8 -0
- package/dist/ui/StatusBar.js +12 -3
- package/dist/ui/entries/CompactionCard.d.ts +5 -4
- package/dist/ui/entries/CompactionCard.js +5 -4
- package/dist/ui/layout/header-hints.d.ts +68 -0
- package/dist/ui/layout/header-hints.js +138 -0
- package/dist/ui/layout/session-chip.d.ts +52 -0
- package/dist/ui/layout/session-chip.js +64 -0
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +8 -1
- package/dist/ui/overlays/HelpOverlay.js +17 -5
- package/dist/ui/overlays/ModelDiscoveryList.d.ts +23 -0
- package/dist/ui/overlays/ModelDiscoveryList.js +161 -0
- package/dist/ui/overlays/ModelProfileEditor.d.ts +18 -1
- package/dist/ui/overlays/ModelProfileEditor.js +117 -68
- package/dist/ui/overlays/SettingsScreen.d.ts +8 -0
- package/dist/ui/overlays/SettingsScreen.js +34 -2
- package/dist/ui/overlays/model-probe-view.d.ts +128 -0
- package/dist/ui/overlays/model-probe-view.js +225 -0
- package/dist/ui/overlays/model-profile-state.d.ts +9 -1
- package/dist/ui/overlays/model-profile-state.js +10 -0
- package/dist/ui/overlays/use-model-probe.d.ts +30 -0
- package/dist/ui/overlays/use-model-probe.js +109 -0
- package/dist/ui/session-bridge.d.ts +26 -0
- package/dist/ui/session-bridge.js +32 -0
- package/package.json +2 -2
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/clear` - start a new conversation - plus its `/reset` alias and the
|
|
3
|
+
* `/clear screen` form that keeps the old "just wipe the screen" meaning
|
|
4
|
+
* (clear-command-context-reset §3.5 / D-1 / D-2 / D-3).
|
|
5
|
+
*
|
|
6
|
+
* ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`. The one
|
|
7
|
+
* localized string below is written with `\u` escapes for that reason, exactly
|
|
8
|
+
* as the `/reset` body it replaces did.
|
|
9
|
+
*
|
|
10
|
+
* WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
|
|
11
|
+
* reason `/compact` and `/context` do (D-8). `builtins.ts` is 1511 lines against
|
|
12
|
+
* `CLAUDE.md`'s 1000-line cap, and this command is no longer a two-line
|
|
13
|
+
* projection reset - it interrupts a run, waits for it, archives a conversation
|
|
14
|
+
* and then clears. A two-line edit belongs inline; this does not.
|
|
15
|
+
*
|
|
16
|
+
* THE COMMAND'S ONLY JOB IS SEQUENCING AND SPEAKING. Every state change happens
|
|
17
|
+
* on the controller port (`AgentController.resetConversation`, which delegates
|
|
18
|
+
* the ORDER to `agent/conversation-reset.ts`), and every word the user reads is
|
|
19
|
+
* decided here, in one place, in the exact order they will read it.
|
|
20
|
+
*/
|
|
21
|
+
import { getLogger } from '../logging/logger.js';
|
|
22
|
+
/** The toast a successful clear shows. Pinned verbatim by an existing test. */
|
|
23
|
+
const CLEARED_TOAST = 'Started a new conversation.';
|
|
24
|
+
/**
|
|
25
|
+
* Run `/clear` or `/reset`. Both registry entries point HERE (D-2), which is
|
|
26
|
+
* what stops the two from drifting back into "one clears the screen, one clears
|
|
27
|
+
* the session" - the drift that produced the defect this feature fixes.
|
|
28
|
+
*/
|
|
29
|
+
export async function runClearCommand(ctx) {
|
|
30
|
+
const verb = ctx.args.trim().toLowerCase();
|
|
31
|
+
if (verb === 'screen') {
|
|
32
|
+
// The old behaviour, byte for byte (I-3): silent, transcript plus the todo
|
|
33
|
+
// panel, history untouched.
|
|
34
|
+
clearScreenOnly(ctx);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (verb !== '') {
|
|
38
|
+
// Rejected rather than ignored. A command that silently accepts and
|
|
39
|
+
// discards an argument is a surface that lies about what it understood.
|
|
40
|
+
ctx.notify('warn', `Unknown argument "${ctx.args.trim()}" - use /clear [screen].`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// READ BEFORE CLEARING (P1-2 / I-9). `clearAllQueues()` empties
|
|
44
|
+
// `pendingUserSteering`, so a count taken afterwards is always zero - and a
|
|
45
|
+
// notice that says "0 messages cancelled" is worse than no notice at all.
|
|
46
|
+
const cancelled = ctx.state.pendingSteering.length;
|
|
47
|
+
const result = await ctx.controller.resetConversation({
|
|
48
|
+
interrupt: true,
|
|
49
|
+
// A THUNK, NOT A VALUE (P0-1 / §3.3). `ctx.state` is the view as of command
|
|
50
|
+
// entry, and the archive happens AFTER the interrupted engine unwinds - so
|
|
51
|
+
// the last tool cards of the abandoned run are written to the transcript in
|
|
52
|
+
// between. A value captured here would archive everything except the part
|
|
53
|
+
// the user was looking at when they typed the command.
|
|
54
|
+
readTranscript: () => ctx.transcriptEntries?.() ?? ctx.state.entries,
|
|
55
|
+
});
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
// ZERO SIDE EFFECTS (I-4), and a trace (P1-5). The user gets one sentence
|
|
58
|
+
// naming the next action; the log gets the reason, because "my /clear did
|
|
59
|
+
// nothing" is otherwise unanswerable after the fact.
|
|
60
|
+
getLogger().warn('agent', 'clear_still_running', { reason: result.reason });
|
|
61
|
+
ctx.notify('warn', result.message);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// THE VIEW GOES LAST (B-5 / §3.3). `App`'s dispatch wrapper zeroes the run
|
|
65
|
+
// metrics, the interaction phase and the scroll anchor on this action, so
|
|
66
|
+
// dispatching it after the engine has settled is what makes "cleared" mean
|
|
67
|
+
// "nothing is still writing into it".
|
|
68
|
+
ctx.dispatch({ type: 'resetConversation' });
|
|
69
|
+
// THE ONE CONSUMER OF `messagesCleared` (P2-1). It cannot join the toast:
|
|
70
|
+
// `clear-command-todo.test.ts` pins that string verbatim, and a count nobody
|
|
71
|
+
// asked for is noise in the one place the user reads confirmations.
|
|
72
|
+
getLogger().info('agent', 'conversation_cleared', {
|
|
73
|
+
messagesCleared: result.messagesCleared,
|
|
74
|
+
interrupted: result.interrupted,
|
|
75
|
+
});
|
|
76
|
+
ctx.toast('info', CLEARED_TOAST);
|
|
77
|
+
// REINSTATED FROM THE OLD `/reset` (P1-1 / C-3). Dropping it would silently
|
|
78
|
+
// delete a user-visible acknowledgement that already existed and was already
|
|
79
|
+
// pinned by test.
|
|
80
|
+
if (cancelled) {
|
|
81
|
+
ctx.notify('warn', `\u5df2\u53d6\u6d88 ${cancelled} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
|
|
82
|
+
}
|
|
83
|
+
if (result.archivedPath) {
|
|
84
|
+
// A PATH RATHER THAN A PROMISE: the user can copy it, and `/resume` accepts
|
|
85
|
+
// an absolute path. "Archived" with no way to find the file is a reassurance,
|
|
86
|
+
// not a recovery.
|
|
87
|
+
ctx.notify('info', `Previous conversation archived: ${result.archivedPath} (/resume <path> to restore)`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Today's `/clear`, unchanged and silent (D-3 / I-3).
|
|
92
|
+
*
|
|
93
|
+
* Kept as a real form rather than as a separate command because "clear the
|
|
94
|
+
* screen" and "clear the conversation" are two different intentions, and a user
|
|
95
|
+
* who wants the first must not have to risk the second. It also gives
|
|
96
|
+
* `clearTranscript` a live dispatcher: `CLAUDE.md` forbids orphaned actions, and
|
|
97
|
+
* this was the last one that would have had none.
|
|
98
|
+
*/
|
|
99
|
+
function clearScreenOnly(ctx) {
|
|
100
|
+
// A USER OVERRIDE OF A PROJECTION, the same class as `/todo clear`, so it
|
|
101
|
+
// reuses reason `'user'`; the distinction against `clearMessages()`'s `'reset'`
|
|
102
|
+
// is recorded on `TodoStore`. Clearing the STORE and not merely the view
|
|
103
|
+
// mirror is what keeps `/save`, `/todo status|continue` and follow-through
|
|
104
|
+
// agreeing with the screen - they read the store directly.
|
|
105
|
+
ctx.controller.clearTodos();
|
|
106
|
+
ctx.dispatch({ type: 'clearTranscript' });
|
|
107
|
+
}
|
|
@@ -6,10 +6,12 @@ import type { SubmitMessageOptions } from '../agent/prompt-options.js';
|
|
|
6
6
|
import type { CopyRequest, CopyRequestResult } from '../ui/clipboard-task.js';
|
|
7
7
|
import type { AgentController } from '../agent/controller.js';
|
|
8
8
|
import type { AgentMode } from '../agent/agent-mode.js';
|
|
9
|
-
import type { NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
|
|
9
|
+
import type { Entry, NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
|
|
10
10
|
import type { PersistedConfig } from '../config/schema.js';
|
|
11
11
|
import type { FollowThroughBudget } from '../todo/follow-through.js';
|
|
12
12
|
import type { UpdateCommandPort } from '../update/types.js';
|
|
13
|
+
import type { SessionCommandPort } from './session-command.js';
|
|
14
|
+
import type { BranchCommandPort } from './branch-command.js';
|
|
13
15
|
export interface CommandContext {
|
|
14
16
|
/** App-owned shared lock and single feedback path for command and selection copies. */
|
|
15
17
|
requestCopy?: (request: CopyRequest) => Promise<CopyRequestResult>;
|
|
@@ -108,6 +110,51 @@ export interface CommandContext {
|
|
|
108
110
|
* stream is handed over directly.
|
|
109
111
|
*/
|
|
110
112
|
writeForeign?: (text: string) => void;
|
|
113
|
+
/**
|
|
114
|
+
* The LIVE transcript rows, for `/clear`'s archive (clear-command-context-reset
|
|
115
|
+
* P0-1 / §3.5).
|
|
116
|
+
*
|
|
117
|
+
* A THUNK, AND THAT IS THE WHOLE POINT. The `state` field above is a snapshot
|
|
118
|
+
* of `ViewState` taken when the command started; `/clear` interrupts a run and
|
|
119
|
+
* then waits for the engine to unwind, so the last tool cards of that run are
|
|
120
|
+
* written to the transcript AFTER `state` was captured. Reading `state.entries`
|
|
121
|
+
* would archive everything except the part the user was just looking at.
|
|
122
|
+
*
|
|
123
|
+
* OPTIONAL, like `followBudget` / `update` / `mouse`: every existing
|
|
124
|
+
* `CommandContext` fixture in this package builds the object by literal, and a
|
|
125
|
+
* required field would break all of them at compile time for a feature none of
|
|
126
|
+
* them are about. Absent means the archive degrades to history-only.
|
|
127
|
+
*
|
|
128
|
+
* `Entry` arrives as a TYPE-ONLY import so this module keeps its runtime shape
|
|
129
|
+
* (the same rule the `update/types.js` import above follows).
|
|
130
|
+
*/
|
|
131
|
+
transcriptEntries?: () => Entry[];
|
|
132
|
+
/**
|
|
133
|
+
* The session's read-only identity plus the two actions `/session` and
|
|
134
|
+
* `/resume` need (tui-session-id-resume 3.11 / D-14 / D-15).
|
|
135
|
+
*
|
|
136
|
+
* OPTIONAL, like `followBudget` / `update` / `mouse` / `transcriptEntries`
|
|
137
|
+
* above: every existing `CommandContext` fixture in this package builds this
|
|
138
|
+
* object by literal, and a required field would break all of them at compile
|
|
139
|
+
* time for a feature none of them is about. Absent means the build has no
|
|
140
|
+
* session identity, and `/session` says exactly that rather than pretending.
|
|
141
|
+
*/
|
|
142
|
+
session?: SessionCommandPort;
|
|
143
|
+
/**
|
|
144
|
+
* The `/branch` port: open a new terminal on this session (tui-branch-terminal-
|
|
145
|
+
* shortcut 3.11 / #8).
|
|
146
|
+
*
|
|
147
|
+
* OPTIONAL for the same reason `session` above is: every existing
|
|
148
|
+
* `CommandContext` fixture in this package builds this object by literal, and a
|
|
149
|
+
* required field would break all of them at compile time for a feature none of
|
|
150
|
+
* them is about. Absent means `/branch` says "not available in this build"
|
|
151
|
+
* rather than failing silently.
|
|
152
|
+
*
|
|
153
|
+
* TYPE-ONLY import, following the rule the `session-command.js` import above
|
|
154
|
+
* records: the command needs the SHAPE of the port and of its result, never a
|
|
155
|
+
* value at run time.
|
|
156
|
+
*/
|
|
157
|
+
branch?: BranchCommandPort;
|
|
111
158
|
}
|
|
112
159
|
/** What `/mouse` can see and do. Read at command time, never captured. */
|
|
113
160
|
export interface MouseCommandPort {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/session` - report this session's identity, and the two things a user does
|
|
3
|
+
* with it (tui-session-id-resume §3.11 / D-13 / D-14).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
|
|
8
|
+
* reason `/clear` and `/compact` do. `builtins.ts` is already past the
|
|
9
|
+
* 1000-line guideline, and this command is a report, not a one-liner.
|
|
10
|
+
*
|
|
11
|
+
* COPYING THE RESUME COMMAND IS THE POINT, NOT A CONVENIENCE (D-14). Typing 6
|
|
12
|
+
* characters off a status bar is the only step of this whole feature that can
|
|
13
|
+
* fail for a human, and `/session copy` removes it: one keystroke, the exact
|
|
14
|
+
* command in the clipboard, no transcription.
|
|
15
|
+
*/
|
|
16
|
+
import type { CheckpointResult } from '../session/checkpoint.js';
|
|
17
|
+
import type { SessionIdentity } from '../session/startup-session.js';
|
|
18
|
+
import type { CommandContext } from './registry.js';
|
|
19
|
+
/**
|
|
20
|
+
* What `/session` can see and do.
|
|
21
|
+
*
|
|
22
|
+
* THREE MEMBERS AND NO MORE. `identity()` is read at command time rather than
|
|
23
|
+
* captured, because `/resume` rotates the id under a live App (D-15) and a
|
|
24
|
+
* captured value would report the previous one for the rest of the session.
|
|
25
|
+
*/
|
|
26
|
+
export interface SessionCommandPort {
|
|
27
|
+
/** The current identity, or `undefined` when this build has none. */
|
|
28
|
+
identity(): SessionIdentity | undefined;
|
|
29
|
+
/** Write once now; `pin` also marks the file as never-prunable. */
|
|
30
|
+
checkpoint(opts?: {
|
|
31
|
+
pin?: boolean;
|
|
32
|
+
}): CheckpointResult;
|
|
33
|
+
/** Adopt another conversation and move to a derived id; `undefined` if it cannot. */
|
|
34
|
+
rotate(loadedPath: string): SessionIdentity | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The one path an id maps to.
|
|
38
|
+
*
|
|
39
|
+
* THROUGH `sessionPathFor` RATHER THAN A SECOND `join` (§2.2): that function is
|
|
40
|
+
* the single choke point where an id becomes a file name, and a report that
|
|
41
|
+
* built its own path would be the first thing to disagree with the writer about
|
|
42
|
+
* where a session lives.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sessionFilePath(id: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Run `/session`, `/session copy` or `/session pin`.
|
|
47
|
+
*
|
|
48
|
+
* ONE NOTICE FOR THE WHOLE REPORT (§3.11 / R-20): six separate notices would put
|
|
49
|
+
* six cards in the transcript and replay them as six fragments on exit. The
|
|
50
|
+
* `file` line is NOT truncated even when it overflows: it is a path the user
|
|
51
|
+
* needs to be able to copy, and it is in the same family as `/save`'s
|
|
52
|
+
* `Saved session to <path>`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function runSessionCommand(ctx: CommandContext): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/session` - report this session's identity, and the two things a user does
|
|
3
|
+
* with it (tui-session-id-resume §3.11 / D-13 / D-14).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
|
|
8
|
+
* reason `/clear` and `/compact` do. `builtins.ts` is already past the
|
|
9
|
+
* 1000-line guideline, and this command is a report, not a one-liner.
|
|
10
|
+
*
|
|
11
|
+
* COPYING THE RESUME COMMAND IS THE POINT, NOT A CONVENIENCE (D-14). Typing 6
|
|
12
|
+
* characters off a status bar is the only step of this whole feature that can
|
|
13
|
+
* fail for a human, and `/session copy` removes it: one keystroke, the exact
|
|
14
|
+
* command in the clipboard, no transcription.
|
|
15
|
+
*/
|
|
16
|
+
import { sessionPathFor } from '../session/store.js';
|
|
17
|
+
/**
|
|
18
|
+
* The one path an id maps to.
|
|
19
|
+
*
|
|
20
|
+
* THROUGH `sessionPathFor` RATHER THAN A SECOND `join` (§2.2): that function is
|
|
21
|
+
* the single choke point where an id becomes a file name, and a report that
|
|
22
|
+
* built its own path would be the first thing to disagree with the writer about
|
|
23
|
+
* where a session lives.
|
|
24
|
+
*/
|
|
25
|
+
export function sessionFilePath(id) {
|
|
26
|
+
return sessionPathFor(id);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Run `/session`, `/session copy` or `/session pin`.
|
|
30
|
+
*
|
|
31
|
+
* ONE NOTICE FOR THE WHOLE REPORT (§3.11 / R-20): six separate notices would put
|
|
32
|
+
* six cards in the transcript and replay them as six fragments on exit. The
|
|
33
|
+
* `file` line is NOT truncated even when it overflows: it is a path the user
|
|
34
|
+
* needs to be able to copy, and it is in the same family as `/save`'s
|
|
35
|
+
* `Saved session to <path>`.
|
|
36
|
+
*/
|
|
37
|
+
export function runSessionCommand(ctx) {
|
|
38
|
+
const port = ctx.session;
|
|
39
|
+
if (!port) {
|
|
40
|
+
// HONEST RATHER THAN SILENT: a build without the feature should say so,
|
|
41
|
+
// because "nothing happened" reads as a bug.
|
|
42
|
+
ctx.notify('warn', 'This session has no id (session recording is off for this build).');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const verb = ctx.args.trim().toLowerCase();
|
|
46
|
+
const identity = port.identity();
|
|
47
|
+
if (!identity) {
|
|
48
|
+
ctx.notify('warn', 'This session has no id (session recording is off for this build).');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (verb === 'copy') {
|
|
52
|
+
copyCommand(ctx, identity);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (verb === 'pin') {
|
|
56
|
+
pinSession(ctx, port, identity);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (verb !== '') {
|
|
60
|
+
// Rejected rather than ignored: a command that silently discards an argument
|
|
61
|
+
// is a surface that lies about what it understood.
|
|
62
|
+
ctx.notify('warn', `Unknown argument "${ctx.args.trim()}" - use /session [copy|pin].`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
report(ctx, identity);
|
|
66
|
+
}
|
|
67
|
+
/** The six-line report. Pure read: no checkpoint, no toast, no dispatch. */
|
|
68
|
+
function report(ctx, identity) {
|
|
69
|
+
const resume = `aragon --resume ${identity.id}`;
|
|
70
|
+
const lines = [
|
|
71
|
+
`id ${identity.id}`,
|
|
72
|
+
`file ${sessionFilePath(identity.id)}`,
|
|
73
|
+
identity.parentId
|
|
74
|
+
? `branch ${identity.parentId} (this session does not write to it)`
|
|
75
|
+
: 'branch (none - this session started here)',
|
|
76
|
+
`pinned ${identity.pinned ? 'yes' : 'no'}`,
|
|
77
|
+
`resume ${resume}`,
|
|
78
|
+
'save as aragon --save <new-id>',
|
|
79
|
+
];
|
|
80
|
+
ctx.notify('info', lines.join('\n'));
|
|
81
|
+
}
|
|
82
|
+
/** `/session copy` - the exact command, through the SAME door `/copy` uses. */
|
|
83
|
+
function copyCommand(ctx, identity) {
|
|
84
|
+
const text = `aragon --resume ${identity.id}`;
|
|
85
|
+
if (!ctx.requestCopy) {
|
|
86
|
+
ctx.notify('warn', 'Clipboard coordinator unavailable.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
void ctx.requestCopy({ text, lines: 1 }).then((result) => {
|
|
90
|
+
if (result.status === 'confirmed' || result.status === 'sent') {
|
|
91
|
+
ctx.toast('info', `Copied ${text}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// A failed or refused copy must not look like a successful one: the command
|
|
95
|
+
// is short enough to read off the report above, so naming the failure is
|
|
96
|
+
// enough to recover.
|
|
97
|
+
ctx.notify('warn', result.status === 'busy' ? 'Clipboard is busy - try /session copy again.' :
|
|
98
|
+
`Copy failed: ${result.reason}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** `/session pin` - write once and stop `sessions prune` from ever touching it. */
|
|
102
|
+
function pinSession(ctx, port, identity) {
|
|
103
|
+
const result = port.checkpoint({ pin: true });
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
ctx.notify('warn', `Could not pin this session: ${result.message}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
ctx.toast('info', `Session ${identity.id} pinned: aragon sessions prune will not delete it.`);
|
|
109
|
+
}
|
package/dist/config/load.d.ts
CHANGED
|
@@ -228,6 +228,21 @@ export interface CliFlags {
|
|
|
228
228
|
compaction?: boolean;
|
|
229
229
|
/** `--compaction-threshold <0..1|N%>` — session override for `threshold`. */
|
|
230
230
|
compactionThreshold?: string;
|
|
231
|
+
/**
|
|
232
|
+
* `--resume <id|path>` - continue a saved session under a NEW id
|
|
233
|
+
* (tui-session-id-resume 4).
|
|
234
|
+
*
|
|
235
|
+
* A FLAG FIELD AND NOT A CONFIG KEY (D-19). There is no persisted "always
|
|
236
|
+
* resume this session", and no switch that shows an id without writing it:
|
|
237
|
+
* that state - an id a user can copy and cannot use - is worse than having no
|
|
238
|
+
* id at all.
|
|
239
|
+
*
|
|
240
|
+
* Carried through `toFlags` like every other flag; a field that reaches only
|
|
241
|
+
* `CliFlags` is inert with no compile error and no runtime error.
|
|
242
|
+
*/
|
|
243
|
+
resumeSession?: string;
|
|
244
|
+
/** `--save <id>` - name THIS run's persistent session and pin it. */
|
|
245
|
+
saveSessionAs?: string;
|
|
231
246
|
}
|
|
232
247
|
/**
|
|
233
248
|
* Resolve the effective config by merging all layers, then derive the runtime
|
|
@@ -34,6 +34,14 @@ export interface ProfileValidationIssue {
|
|
|
34
34
|
export declare function emptyModelProfiles(): ModelProfilesConfig;
|
|
35
35
|
/** Return independently mutable role state. */
|
|
36
36
|
export declare function emptyModelProfileState(invalid?: boolean): ModelProfilesRuntime;
|
|
37
|
+
/**
|
|
38
|
+
* The endpoint rule published as a predicate (model-profile-connection-test §5 row 10).
|
|
39
|
+
*
|
|
40
|
+
* The probe's `invalid_base_url` guard has to use THE SAME ruler as validation, or a
|
|
41
|
+
* URL the editor accepts can still be refused by the test (and the reverse). A second
|
|
42
|
+
* copy of these four clauses is one edit away from disagreeing with the config layer.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isValidEndpointUrl(value: string | null | undefined): boolean;
|
|
37
45
|
/** Validate untrusted input; diagnostics never include supplied values. */
|
|
38
46
|
export declare function validateModelProfiles(value: unknown): ProfileValidationIssue[];
|
|
39
47
|
/**
|
|
@@ -32,6 +32,16 @@ function validUrl(value) {
|
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The endpoint rule published as a predicate (model-profile-connection-test §5 row 10).
|
|
37
|
+
*
|
|
38
|
+
* The probe's `invalid_base_url` guard has to use THE SAME ruler as validation, or a
|
|
39
|
+
* URL the editor accepts can still be refused by the test (and the reverse). A second
|
|
40
|
+
* copy of these four clauses is one edit away from disagreeing with the config layer.
|
|
41
|
+
*/
|
|
42
|
+
export function isValidEndpointUrl(value) {
|
|
43
|
+
return validUrl(value);
|
|
44
|
+
}
|
|
35
45
|
function validateEntry(value, index) {
|
|
36
46
|
const path = `modelProfiles.entries[${index}]`;
|
|
37
47
|
const invalid = (field, message) => ({
|
package/dist/exec/index.js
CHANGED
|
@@ -418,7 +418,16 @@ function planSession(options, cwd, err) {
|
|
|
418
418
|
if (options.sessionId !== undefined) {
|
|
419
419
|
// THE UPSERT. A wrapper that mints its own id must not have to ask whether
|
|
420
420
|
// this is the first turn; that is the whole reason this form exists.
|
|
421
|
-
|
|
421
|
+
//
|
|
422
|
+
// AND IT IS A RESUME FORM, so the same write-back rule applies (tui-session-
|
|
423
|
+
// id-resume D-20 / I-3). When a file already exists under that id,
|
|
424
|
+
// `resumedSessionId` decides which id this run writes - and for a TUI
|
|
425
|
+
// checkpoint that answer is a NEW uuid. Without this line the wrapper-friendly
|
|
426
|
+
// upsert was the one remaining way to reach a TUI checkpoint and replace it,
|
|
427
|
+
// which is silent, unrecoverable loss of the user's live conversation.
|
|
428
|
+
const path = sessionPathFor(options.sessionId);
|
|
429
|
+
const saved = readSessionFile(path);
|
|
430
|
+
return { id: saved ? resumedSessionId(path, saved) : options.sessionId, saved, saving, cwd };
|
|
422
431
|
}
|
|
423
432
|
return { id: randomUUID(), saved: null, saving, cwd };
|
|
424
433
|
}
|
package/dist/logging/logger.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import type { LogConfig } from '../config/schema.js';
|
|
24
24
|
import { type LogLevelName } from './levels.js';
|
|
25
25
|
/** Closed set, not free text: a typo'd scope is a record nobody can filter on. */
|
|
26
|
-
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update' | 'compaction';
|
|
26
|
+
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update' | 'compaction' | 'session' | 'branch';
|
|
27
27
|
export interface LogRecord {
|
|
28
28
|
ts: string;
|
|
29
29
|
lv: LogLevelName;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a new terminal window looks like from the outside (tui-branch-terminal-shortcut
|
|
3
|
+
* 3.7).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/proc/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* PURE, AND THAT IS THE POINT. Three platforms, at least ten terminal emulators,
|
|
8
|
+
* four exec-argument conventions and Windows' three layers of parsing (we -> `cmd`
|
|
9
|
+
* -> `PowerShell`) all collapse into "what should we spawn, with which argv". A
|
|
10
|
+
* command body that called `spawn('cmd', ['/c','start', ...])` inline would bury
|
|
11
|
+
* the only testable part of this feature - the command line itself - inside
|
|
12
|
+
* untestable IO. `proc/readiness.ts` and `proc/classify.ts` set the same precedent
|
|
13
|
+
* inside this directory.
|
|
14
|
+
*
|
|
15
|
+
* WHY SESSION CODE MAY DEPEND ON THIS MODULE: it imports nothing from
|
|
16
|
+
* `node:child_process`, reads no configuration and creates no process. The single
|
|
17
|
+
* place a process is created stays in `session/branch-launch.ts`, so `proc/` keeps
|
|
18
|
+
* meaning "processes" and `session/` keeps meaning "sessions".
|
|
19
|
+
*
|
|
20
|
+
* EVERY RUNG CARRIES ITS OWN WORKING DIRECTORY (D-16 / D-23 / I-9). This is the
|
|
21
|
+
* one thing a reviewer will assume is free and is not: `spawn({ cwd })` decides the
|
|
22
|
+
* cwd of the process WE start, and for `wt -w 0 nt` (a new tab in an already
|
|
23
|
+
* running Windows Terminal) or `osascript` (a new Terminal.app window) that
|
|
24
|
+
* process merely forwards the request - the window opens in the terminal's own
|
|
25
|
+
* default directory. So `wt` passes `-d`, and both script rungs start with
|
|
26
|
+
* `cd <cwd> &&`. `--resume` does not restore a cwd, so a window that opens in the
|
|
27
|
+
* wrong place can never be corrected from inside the branch.
|
|
28
|
+
*/
|
|
29
|
+
/** The three ladders, collapsed from `NodeJS.Platform` (win32 / darwin / everything else). */
|
|
30
|
+
export type TerminalPlatform = 'win32' | 'darwin' | 'posix';
|
|
31
|
+
export interface ShellInvocation {
|
|
32
|
+
/** Executable: an absolute path or a name resolved through PATH. */
|
|
33
|
+
path: string;
|
|
34
|
+
/** Which command syntax the aragon command has to be encoded for. */
|
|
35
|
+
kind: 'powershell' | 'posix';
|
|
36
|
+
}
|
|
37
|
+
export interface TerminalLaunchPlan {
|
|
38
|
+
/** The program we spawn. */
|
|
39
|
+
file: string;
|
|
40
|
+
args: string[];
|
|
41
|
+
/** True only for the `cmd` rung, whose command line `cmd.exe` must see verbatim. */
|
|
42
|
+
windowsVerbatimArguments: boolean;
|
|
43
|
+
/** Ladder name, for the log and for the success toast's debug detail. */
|
|
44
|
+
rung: string;
|
|
45
|
+
}
|
|
46
|
+
export type TerminalPlanResult = {
|
|
47
|
+
ok: true;
|
|
48
|
+
plans: TerminalLaunchPlan[];
|
|
49
|
+
shell: ShellInvocation;
|
|
50
|
+
manual: string;
|
|
51
|
+
} | {
|
|
52
|
+
ok: false;
|
|
53
|
+
reason: 'no-launcher';
|
|
54
|
+
shell: ShellInvocation;
|
|
55
|
+
manual: string;
|
|
56
|
+
};
|
|
57
|
+
export interface TerminalPlanInput {
|
|
58
|
+
platform: NodeJS.Platform;
|
|
59
|
+
env: Record<string, string | undefined>;
|
|
60
|
+
pathDirs: readonly string[];
|
|
61
|
+
isExecutable?: (name: string) => boolean;
|
|
62
|
+
shell: ShellInvocation;
|
|
63
|
+
/** The aragon command's argv pieces (`session/branch-invocation.ts`'s product). */
|
|
64
|
+
command: readonly string[];
|
|
65
|
+
/** New window title, ASCII, already truncated to the cell budget. */
|
|
66
|
+
title: string;
|
|
67
|
+
cwd: string;
|
|
68
|
+
/** `joinCommandLine(command)` - the pasteable fallback shown to the user. */
|
|
69
|
+
manual: string;
|
|
70
|
+
}
|
|
71
|
+
/** The env var that overrides the shell the new window runs (D-7). */
|
|
72
|
+
export declare const ARAGON_BRANCH_SHELL_ENV = "ARAGON_BRANCH_SHELL";
|
|
73
|
+
/**
|
|
74
|
+
* Build the ladder for this platform, most-preferred rung first.
|
|
75
|
+
*
|
|
76
|
+
* The return value is a LIST because the failures this feature can actually hit
|
|
77
|
+
* are synchronous argument rejections (ENOENT on `wt.exe`, no X display), and a
|
|
78
|
+
* list lets `branch-launch.ts` walk down it without knowing anything about
|
|
79
|
+
* terminals. Rung COUNT is bounded by the caller (`BRANCH_TERMINAL_LIMITS.maxRungs`),
|
|
80
|
+
* so there is exactly one place that number lives.
|
|
81
|
+
*/
|
|
82
|
+
export declare function planTerminalLaunch(input: TerminalPlanInput): TerminalPlanResult;
|
|
83
|
+
/** PowerShell: the whole command as one base64(UTF-16LE) blob (D-20). */
|
|
84
|
+
export declare function powerShellScript(parts: readonly string[]): string;
|
|
85
|
+
/** Bare words stay bare; everything else becomes a single-quoted literal. */
|
|
86
|
+
export declare function powerShellQuote(value: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* `-EncodedCommand` takes base64 of UTF-16LE, which needs no shell quoting at all.
|
|
89
|
+
*
|
|
90
|
+
* The alphabet is `A-Za-z0-9+/=` - no spaces, no quotes, no `cmd` metacharacters -
|
|
91
|
+
* so the blob survives all three of Windows' parsers as an ordinary word. This is
|
|
92
|
+
* the same trick `ui/win-vt-force.ts` uses, for the same reason: encode the fragile
|
|
93
|
+
* thing into a form nothing can argue with, at construction time. Our command line
|
|
94
|
+
* necessarily contains spaced paths (`C:\Program Files\nodejs\node.exe`), so the
|
|
95
|
+
* fragile thing is not hypothetical.
|
|
96
|
+
*/
|
|
97
|
+
export declare function encodePowerShellCommand(script: string): string;
|
|
98
|
+
/** POSIX single-quote escaping: `'` becomes `'\''`. */
|
|
99
|
+
export declare function posixQuote(value: string): string;
|
|
100
|
+
export declare function posixScript(parts: readonly string[]): string;
|
|
101
|
+
/** AppleScript string literals only need `\` and `"` escaped. */
|
|
102
|
+
export declare function escapeAppleScriptText(value: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Win32 argv quoting for the `cmd` rung's title and shell path: quote when it
|
|
105
|
+
* contains whitespace, otherwise leave it alone.
|
|
106
|
+
*
|
|
107
|
+
* THE INPUT DOMAIN EXCLUDES `"` BY CONSTRUCTION (R-17/T-48): Windows paths cannot
|
|
108
|
+
* contain it, and the title is `aragon <id>` where an id is `[A-Za-z0-9._-]`. If a
|
|
109
|
+
* future source can contain a quote, this must switch to `cmd`'s rule (`""`) and
|
|
110
|
+
* not to the POSIX `\"` written here - the two are not interchangeable.
|
|
111
|
+
*/
|
|
112
|
+
export declare function quoteWin32Arg(value: string): string;
|
|
113
|
+
/** PATH entries for `name` probing: both spellings on Windows, `:` on POSIX. */
|
|
114
|
+
export declare function pathDirsOf(env: Record<string, string | undefined>, platform: NodeJS.Platform): string[];
|
|
115
|
+
/**
|
|
116
|
+
* The default probe: is this a file we could execute?
|
|
117
|
+
*
|
|
118
|
+
* An absolute or separator-bearing value is stat'd directly (that is the
|
|
119
|
+
* `ARAGON_BRANCH_SHELL=/usr/local/bin/fish` case); a bare name is looked up in the
|
|
120
|
+
* PATH directories, trying `name` and - on Windows - `name + '.exe'`, because a
|
|
121
|
+
* user typing `pwsh` and a probe looking for `pwsh.exe` are the same request.
|
|
122
|
+
*
|
|
123
|
+
* EVERY EXCEPTION RETURNS FALSE: one failed stat means "this candidate is not
|
|
124
|
+
* here", not "the feature is broken", and a missing terminal must degrade to the
|
|
125
|
+
* manual command rather than throw out of a key handler.
|
|
126
|
+
*/
|
|
127
|
+
export declare function defaultIsExecutable(platform: NodeJS.Platform, pathDirs?: readonly string[]): (name: string) => boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Which shell the new window runs (D-7), first match wins.
|
|
130
|
+
*
|
|
131
|
+
* The requirement says "PowerShell / bash", and both platforms have two answers
|
|
132
|
+
* worth distinguishing: PowerShell 7 (`pwsh.exe`) when the user installed it,
|
|
133
|
+
* Windows PowerShell 5.1 otherwise; `$SHELL` when the user's login shell is not
|
|
134
|
+
* bash, then bash, then `/bin/sh` as the one path that exists everywhere.
|
|
135
|
+
* `ARAGON_BRANCH_SHELL` is the escape hatch - an ENV VALUE, not a config key
|
|
136
|
+
* (D-17): a "Ctrl+L opens no terminal" switch would create the one state this
|
|
137
|
+
* feature must never have.
|
|
138
|
+
*/
|
|
139
|
+
export declare function resolveShell(input: {
|
|
140
|
+
platform: NodeJS.Platform;
|
|
141
|
+
env: Record<string, string | undefined>;
|
|
142
|
+
isExecutable?: (name: string) => boolean;
|
|
143
|
+
pathDirs: readonly string[];
|
|
144
|
+
}): ShellInvocation;
|
|
145
|
+
/**
|
|
146
|
+
* Fit a window title into `maxCells` columns (R-7 / T-47).
|
|
147
|
+
*
|
|
148
|
+
* THE TITLE IS THE ONLY THING THIS TOUCHES. An id can be 64 characters long, so
|
|
149
|
+
* `aragon <id>` exceeds the 60-cell budget by construction for the longest ids -
|
|
150
|
+
* and truncating the id anywhere else (the command line, `--resume`) would produce
|
|
151
|
+
* a branch that cannot find its own parent. Code points, not bytes: the title is
|
|
152
|
+
* ASCII today, and a future non-ASCII title must not be cut through a surrogate
|
|
153
|
+
* pair.
|
|
154
|
+
*/
|
|
155
|
+
export declare function truncateTitle(title: string, maxCells: number): string;
|