aiden-runtime 4.1.2 → 4.1.3
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/dist/cli/v4/aidenCLI.js +10 -0
- package/dist/cli/v4/callbacks.js +15 -0
- package/dist/cli/v4/chatSession.js +120 -2
- package/dist/cli/v4/commands/doctor.js +23 -27
- package/dist/cli/v4/commands/model.js +30 -1
- package/dist/cli/v4/display/capabilityCard.js +135 -0
- package/dist/cli/v4/display/sessionEndCard.js +127 -0
- package/dist/cli/v4/display/toolTrail.js +172 -0
- package/dist/cli/v4/display.js +464 -132
- package/dist/cli/v4/doctor.js +377 -75
- package/dist/cli/v4/promotionPrompt.js +135 -5
- package/dist/cli/v4/replyRenderer.js +311 -20
- package/dist/cli/v4/skinEngine.js +14 -3
- package/dist/cli/v4/toolPreview.js +14 -0
- package/dist/core/tools/nowPlaying.js +7 -15
- package/dist/core/v4/sessionDistiller.js +48 -1
- package/dist/core/v4/toolRegistry.js +16 -1
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +19 -0
- package/dist/providers/v4/errors.js +92 -0
- package/dist/tools/v4/index.js +24 -1
- package/dist/tools/v4/sessions/recallSession.js +14 -0
- package/dist/tools/v4/system/_psHelpers.js +70 -2
- package/dist/tools/v4/system/appInput.js +154 -0
- package/dist/tools/v4/system/appLaunch.js +136 -10
- package/dist/tools/v4/system/mediaKey.js +35 -4
- package/dist/tools/v4/system/mediaSessions.js +163 -0
- package/dist/tools/v4/system/mediaTransport.js +211 -0
- package/package.json +1 -1
- package/skills/system_control.md +56 -6
package/dist/cli/v4/aidenCLI.js
CHANGED
|
@@ -1449,6 +1449,12 @@ async function buildAgentRuntime(cliOpts, opts) {
|
|
|
1449
1449
|
mcpClient,
|
|
1450
1450
|
providerId,
|
|
1451
1451
|
modelId,
|
|
1452
|
+
// v4.1.3-prebump: forward the precedence-case label so the boot
|
|
1453
|
+
// card can render a "where this choice came from" annotation.
|
|
1454
|
+
// The case-3 (persisted-config) branch was confusing users who
|
|
1455
|
+
// expected auto-pick to kick in — surfacing the source closes the
|
|
1456
|
+
// information asymmetry.
|
|
1457
|
+
bootSource,
|
|
1452
1458
|
resumeSessionId,
|
|
1453
1459
|
fallbackAdapter,
|
|
1454
1460
|
personalityManager,
|
|
@@ -1475,6 +1481,10 @@ async function runInteractiveChat(cliOpts, opts) {
|
|
|
1475
1481
|
config: runtime.config,
|
|
1476
1482
|
initialProviderId: runtime.providerId,
|
|
1477
1483
|
initialModelId: runtime.modelId,
|
|
1484
|
+
// v4.1.3-prebump: pass through the precedence-case label so the
|
|
1485
|
+
// boot card can render a dim source annotation under the version
|
|
1486
|
+
// pill ("persisted from prior session" / "auto-picked" / …).
|
|
1487
|
+
initialBootSource: runtime.bootSource,
|
|
1478
1488
|
resumeSessionId: runtime.resumeSessionId,
|
|
1479
1489
|
yoloMode: !!cliOpts.yolo,
|
|
1480
1490
|
fallbackAdapter: runtime.fallbackAdapter,
|
package/dist/cli/v4/callbacks.js
CHANGED
|
@@ -102,6 +102,21 @@ class CliCallbacks {
|
|
|
102
102
|
}
|
|
103
103
|
if (err) {
|
|
104
104
|
handle.fail(ms);
|
|
105
|
+
// v4.1.3-essentials: when the tool's failure payload includes a
|
|
106
|
+
// structured capability card (auth missing, platform unsupported),
|
|
107
|
+
// render the card immediately after the fail row. The card sits
|
|
108
|
+
// on its own multi-line block — the fail row is still useful as
|
|
109
|
+
// the action timeline anchor; the card adds the state assessment
|
|
110
|
+
// the user actually needs. No card → plain failure surface.
|
|
111
|
+
if (result?.capabilityCard) {
|
|
112
|
+
this.display.capabilityCard(result.capabilityCard);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// v4.1.3-repl-polish: degraded outcome — tool completed but with a
|
|
117
|
+
// partial / best-effort result. Show in trail yellow instead of silent.
|
|
118
|
+
if (result?.degraded) {
|
|
119
|
+
handle.degraded(ms, result.degradedReason);
|
|
105
120
|
return;
|
|
106
121
|
}
|
|
107
122
|
handle.ok(ms);
|
|
@@ -59,6 +59,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
59
59
|
exports.BOOT_TRY_HINT = exports.ChatSession = void 0;
|
|
60
60
|
exports.parseSessionBulletsResponse = parseSessionBulletsResponse;
|
|
61
61
|
exports.renderCommandLabel = renderCommandLabel;
|
|
62
|
+
exports.bootSourceLabel = bootSourceLabel;
|
|
62
63
|
exports.detectOS = detectOS;
|
|
63
64
|
exports.detectShell = detectShell;
|
|
64
65
|
exports.formatStatusState = formatStatusState;
|
|
@@ -74,7 +75,12 @@ const sessionSummaryGate_1 = require("./sessionSummaryGate");
|
|
|
74
75
|
const aidenPrompt_1 = __importDefault(require("./aidenPrompt"));
|
|
75
76
|
const historyStore_1 = require("./historyStore");
|
|
76
77
|
const modelMetadata_1 = require("../../core/v4/modelMetadata");
|
|
78
|
+
// v4.1.3-prebump: classify provider errors so the catch path can show
|
|
79
|
+
// a tailored action hint (e.g. groq 413 → "switch to chatgpt-plus")
|
|
80
|
+
// instead of the generic "/model or aiden doctor" line.
|
|
81
|
+
const errors_1 = require("../../providers/v4/errors");
|
|
77
82
|
const sessionDistiller_1 = require("../../core/v4/sessionDistiller");
|
|
83
|
+
const sessionEndCard_1 = require("./display/sessionEndCard");
|
|
78
84
|
const version_1 = require("../../core/version");
|
|
79
85
|
const distillationStore_1 = require("../../core/v4/distillationStore");
|
|
80
86
|
const promotionCandidates_1 = require("../../core/v4/promotionCandidates");
|
|
@@ -154,7 +160,15 @@ const STATUS_BAR_WIDTH = 10;
|
|
|
154
160
|
* Above this we abandon the LLM half (still write a deterministic-
|
|
155
161
|
* only distillation so the session isn't lost) and exit honestly.
|
|
156
162
|
*/
|
|
157
|
-
|
|
163
|
+
/**
|
|
164
|
+
* v4.1.3-essentials distillation-fix: bumped 4000 → 12000ms in
|
|
165
|
+
* lockstep with `sessionDistiller.DEFAULT_TIMEOUT_MS`. Same
|
|
166
|
+
* rationale — chatgpt-plus Codex cold-start latency for 800-token
|
|
167
|
+
* summaries regularly exceeds 4s, killing the distillation +
|
|
168
|
+
* promotion-prompt path. Env override `AIDEN_SUMMARY_TIMEOUT_MS`
|
|
169
|
+
* still respected.
|
|
170
|
+
*/
|
|
171
|
+
const SUMMARY_TIMEOUT_MS_DEFAULT = 12000;
|
|
158
172
|
function resolveSummaryTimeoutMs() {
|
|
159
173
|
const raw = process.env.AIDEN_SUMMARY_TIMEOUT_MS;
|
|
160
174
|
if (!raw)
|
|
@@ -162,6 +176,35 @@ function resolveSummaryTimeoutMs() {
|
|
|
162
176
|
const parsed = Number.parseInt(raw, 10);
|
|
163
177
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : SUMMARY_TIMEOUT_MS_DEFAULT;
|
|
164
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* v4.1.3-prebump: map a providerBootSelector precedence-case label to
|
|
181
|
+
* a human-readable hint rendered under the boot card's status pills.
|
|
182
|
+
*
|
|
183
|
+
* Returns `null` for the explicit-selection cases (`cli-flag`, with-or-
|
|
184
|
+
* without -partial) where the source isn't surprising. Annotates the
|
|
185
|
+
* persisted-config / auto-priority / hardcoded-fallback paths so users
|
|
186
|
+
* understand "why this provider, why now".
|
|
187
|
+
*
|
|
188
|
+
* Pure helper — exported for unit testing.
|
|
189
|
+
*/
|
|
190
|
+
function bootSourceLabel(source) {
|
|
191
|
+
switch (source) {
|
|
192
|
+
case 'persisted-config':
|
|
193
|
+
return '(persisted from prior session — /model to change)';
|
|
194
|
+
case 'config-partial':
|
|
195
|
+
return '(partial config + auto-resolved companion)';
|
|
196
|
+
case 'auto-priority':
|
|
197
|
+
return '(auto-picked — first authed provider)';
|
|
198
|
+
case 'hardcoded-fallback':
|
|
199
|
+
return '(no authed providers — using legacy default)';
|
|
200
|
+
case 'cli-flag':
|
|
201
|
+
case 'cli-flag-partial':
|
|
202
|
+
// Explicit CLI override — user knows why; no annotation.
|
|
203
|
+
return null;
|
|
204
|
+
default:
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
165
208
|
class ChatSession {
|
|
166
209
|
constructor(opts) {
|
|
167
210
|
this.opts = opts;
|
|
@@ -208,6 +251,13 @@ class ChatSession {
|
|
|
208
251
|
* populated alongside it after a verified write.
|
|
209
252
|
*/
|
|
210
253
|
this.lastDistillation = null;
|
|
254
|
+
/**
|
|
255
|
+
* Absolute path the most recent distillation JSON was written to.
|
|
256
|
+
* Captured at write-time and surfaced in the session-end card so the
|
|
257
|
+
* user has a concrete artifact to inspect or feed to recall_session.
|
|
258
|
+
* Null when the write failed or no distillation has been produced.
|
|
259
|
+
*/
|
|
260
|
+
this.lastDistillationPath = null;
|
|
211
261
|
this.currentProviderId = opts.initialProviderId;
|
|
212
262
|
this.currentModelId = opts.initialModelId;
|
|
213
263
|
this.modelMetadata = opts.modelMetadata ?? new modelMetadata_1.ModelMetadata();
|
|
@@ -298,6 +348,14 @@ class ChatSession {
|
|
|
298
348
|
catch (err) {
|
|
299
349
|
this.opts.display.warn(`Session summary skipped on ${sig}: ${err.message}`);
|
|
300
350
|
}
|
|
351
|
+
// v4.1.3-repl-polish: render session-end card before farewell when
|
|
352
|
+
// a distillation was written this session. Pass the on-disk path
|
|
353
|
+
// so the card surfaces the artifact location to the user.
|
|
354
|
+
if (this.lastDistillation) {
|
|
355
|
+
for (const line of (0, sessionEndCard_1.renderSessionEndCard)(this.lastDistillation, (t, k) => this.opts.display.applyColors(t, k), this.lastDistillationPath)) {
|
|
356
|
+
this.opts.display.write(line + '\n');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
301
359
|
this.opts.display.dim('Goodbye.');
|
|
302
360
|
process.exit(0);
|
|
303
361
|
};
|
|
@@ -430,6 +488,12 @@ class ChatSession {
|
|
|
430
488
|
// is resumed in the same process (not today's behavior),
|
|
431
489
|
// otherwise they're skipped — documented in commit.
|
|
432
490
|
await this.maybeRunPromotion(promptApi);
|
|
491
|
+
// v4.1.3-repl-polish: session-end card before farewell.
|
|
492
|
+
if (this.lastDistillation) {
|
|
493
|
+
for (const line of (0, sessionEndCard_1.renderSessionEndCard)(this.lastDistillation, (t, k) => this.opts.display.applyColors(t, k), this.lastDistillationPath)) {
|
|
494
|
+
this.opts.display.write(line + '\n');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
433
497
|
break;
|
|
434
498
|
}
|
|
435
499
|
if (result.clearHistory)
|
|
@@ -555,6 +619,15 @@ class ChatSession {
|
|
|
555
619
|
toolTrace: this.sessionToolTrace,
|
|
556
620
|
auxiliaryClient: this.opts.auxiliaryClient,
|
|
557
621
|
timeoutMs,
|
|
622
|
+
// v4.1.3-essentials distillation-fix: route the new
|
|
623
|
+
// diagnostic signal to a dim line so the user can see WHICH
|
|
624
|
+
// of the three failure classes fired (timeout / call-fail /
|
|
625
|
+
// unparseable JSON). Before this hook, all three converged
|
|
626
|
+
// on a silent `partial:true` and the downstream "no bullets"
|
|
627
|
+
// warning didn't distinguish them.
|
|
628
|
+
onDiagnostic: (msg) => {
|
|
629
|
+
this.opts.display.dim(`[distill] ${msg}`);
|
|
630
|
+
},
|
|
558
631
|
});
|
|
559
632
|
}
|
|
560
633
|
catch (err) {
|
|
@@ -569,6 +642,7 @@ class ChatSession {
|
|
|
569
642
|
const dir = node_path_1.default.join(this.opts.paths.root, 'distillations');
|
|
570
643
|
try {
|
|
571
644
|
const file = await (0, distillationStore_1.writeDistillation)(dir, dist);
|
|
645
|
+
this.lastDistillationPath = file;
|
|
572
646
|
this.opts.display.dim(`Session distillation${dist.partial ? ' (partial)' : ''} saved to ${file}`);
|
|
573
647
|
}
|
|
574
648
|
catch (err) {
|
|
@@ -896,7 +970,42 @@ class ChatSession {
|
|
|
896
970
|
if (streamingActive)
|
|
897
971
|
this.opts.display.streamComplete();
|
|
898
972
|
const msg = err?.message ?? String(err);
|
|
899
|
-
|
|
973
|
+
// v4.1.3-prebump: classify the error so the suggestion below
|
|
974
|
+
// points at the actual fix instead of the generic "/model or
|
|
975
|
+
// doctor" line. 413 / 429 / auth get tailored hints; everything
|
|
976
|
+
// else keeps the legacy fallback. Use the live providerId so
|
|
977
|
+
// the user sees WHICH provider blew up (matters when fallback
|
|
978
|
+
// adapters rotate slots mid-turn).
|
|
979
|
+
const cls = (0, errors_1.classifyProviderError)(err);
|
|
980
|
+
const tailored = (0, errors_1.suggestForErrorClass)(cls, this.currentProviderId);
|
|
981
|
+
// v4.1.3-essentials: on `auth` class errors we have enough state
|
|
982
|
+
// (which provider, what to run) to render a capability card —
|
|
983
|
+
// structured "what auth's missing, what you can still do, how to
|
|
984
|
+
// fix" is more useful than the bare message + one-line hint.
|
|
985
|
+
// Other classes keep the printError single-line surface; their
|
|
986
|
+
// hints are already specific.
|
|
987
|
+
if (cls === 'auth') {
|
|
988
|
+
const p = this.currentProviderId;
|
|
989
|
+
this.opts.display.printError(msg);
|
|
990
|
+
this.opts.display.capabilityCard({
|
|
991
|
+
title: `${p} authentication required`,
|
|
992
|
+
canStill: [
|
|
993
|
+
'Continue chatting if a non-auth provider is configured (run `/model`)',
|
|
994
|
+
'Run `/auth status` to see which providers are signed in',
|
|
995
|
+
'Run `aiden doctor --providers` for a fuller liveness probe',
|
|
996
|
+
],
|
|
997
|
+
cannotReliably: [
|
|
998
|
+
`Call ${p} until credentials are refreshed`,
|
|
999
|
+
'Trust any cached responses that depended on this provider',
|
|
1000
|
+
],
|
|
1001
|
+
fix: `Run \`/auth login ${p}\` if it's an OAuth provider, or set the ` +
|
|
1002
|
+
`relevant API key env var. Then retry — no need to restart Aiden.`,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
this.opts.display.printError(msg, tailored
|
|
1007
|
+
?? 'Run `/model` to switch providers or `aiden doctor` to diagnose.');
|
|
1008
|
+
}
|
|
900
1009
|
this.setStatusState({ kind: 'ready' });
|
|
901
1010
|
this.lastTurnElapsedMs = Date.now() - turnStartedAt;
|
|
902
1011
|
}
|
|
@@ -979,6 +1088,15 @@ class ChatSession {
|
|
|
979
1088
|
providerOk: !this.opts.unconfigured,
|
|
980
1089
|
version: version_1.VERSION,
|
|
981
1090
|
}) + '\n');
|
|
1091
|
+
// v4.1.3-prebump: dim source annotation under the pills row so the
|
|
1092
|
+
// user can see WHY this provider/model was chosen — closes the
|
|
1093
|
+
// information gap that made Case 3 (persisted-config) look like a
|
|
1094
|
+
// bug ("why is it still on groq when I auth'd chatgpt-plus?"). One
|
|
1095
|
+
// line, dim, only when the source is informative.
|
|
1096
|
+
const sourceLabel = bootSourceLabel(this.opts.initialBootSource);
|
|
1097
|
+
if (sourceLabel) {
|
|
1098
|
+
display.write(` ${display.muted(sourceLabel)}\n`);
|
|
1099
|
+
}
|
|
982
1100
|
// Tier-3.1b: rule + environment/capabilities block + rule + scroll
|
|
983
1101
|
// + bottom prompt hint. Skipped at <70 cols to keep the narrow
|
|
984
1102
|
// boot card from wrapping into noise.
|
|
@@ -32,35 +32,31 @@ exports.doctor = {
|
|
|
32
32
|
}
|
|
33
33
|
ctx.display.info('Running diagnostic checks...');
|
|
34
34
|
const report = await (0, doctor_1.runDoctor)({ paths: ctx.paths });
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// `aiden doctor` CLI subcommand correctly omits this — the
|
|
43
|
-
// counters would always be zero there.
|
|
35
|
+
// v4.1.3-essentials doctor-polish: pull in-process subsystem
|
|
36
|
+
// health + skill-outcome data into the same report so they
|
|
37
|
+
// render as additional grouped sections inside the health box,
|
|
38
|
+
// not as disconnected blocks below it. `subsystemHealthResults`
|
|
39
|
+
// / `skillOutcomeResults` return empty arrays when their
|
|
40
|
+
// sources are unavailable so the grouped-renderer simply drops
|
|
41
|
+
// those sections.
|
|
44
42
|
if (ctx.agent) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
ctx.display.write(`[url-provenance] blocked=${u.blocked} recovered=${u.recovered} failed=${u.failed} (session)\n`);
|
|
57
|
-
// Phase 23.4a-fix2: empty-response counters. detected =
|
|
58
|
-
// Codex backend completed a turn with no content and no tool
|
|
59
|
-
// calls; retried = corrective system message injected (cap
|
|
60
|
-
// 1/turn); recovered = retry yielded a non-empty reply.
|
|
61
|
-
const e = ctx.agent.getEmptyResponseMetrics();
|
|
62
|
-
ctx.display.write(`[empty-response] detected=${e.detected} retried=${e.retried} recovered=${e.recovered} (session)\n`);
|
|
43
|
+
const a = ctx.agent;
|
|
44
|
+
report.results.push(...(0, doctor_1.subsystemHealthResults)(a.subsystemHealthRegistry));
|
|
45
|
+
report.results.push(...(0, doctor_1.skillOutcomeResults)(a.skillOutcomeTracker));
|
|
46
|
+
// v4.1.3-essentials doctor-polish: session-scoped counters
|
|
47
|
+
// (skill enforcement / URL provenance / empty response) now
|
|
48
|
+
// fold into the same report so they render as a "Session
|
|
49
|
+
// counters" group INSIDE the box instead of as orphan
|
|
50
|
+
// `display.write` lines below it. Previous code emitted them
|
|
51
|
+
// as 3 separate `[bracket-prefix] key=N ...` lines after
|
|
52
|
+
// renderHealthBox closed — visually disconnected.
|
|
53
|
+
report.results.push(...(0, doctor_1.sessionCounterResults)(ctx.agent));
|
|
63
54
|
}
|
|
55
|
+
// v4.1.3-essentials doctor-polish: renderHealthBox now groups
|
|
56
|
+
// results by section header with a top summary. Same renderer
|
|
57
|
+
// is used by `aiden doctor` CLI path so both surfaces stay in
|
|
58
|
+
// visual sync (Path-A unification).
|
|
59
|
+
ctx.display.write((0, doctor_1.renderHealthBox)(report, ctx.display) + '\n');
|
|
64
60
|
return {};
|
|
65
61
|
},
|
|
66
62
|
};
|
|
@@ -106,7 +106,36 @@ exports.model = {
|
|
|
106
106
|
return {};
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
// v4.1.3-prebump: persist the selection to config.yaml so the NEXT
|
|
110
|
+
// boot honours the user's choice. Without this, `/model` only
|
|
111
|
+
// updated the live session — and the persisted `model.provider /
|
|
112
|
+
// model.modelId` keys (which Case 3 in providerBootSelector
|
|
113
|
+
// consults first) silently kept their stale values from the
|
|
114
|
+
// previous wizard run. Result: every reboot snapped the user back
|
|
115
|
+
// to the wizard's original pick (typically groq + llama-3.3-70b),
|
|
116
|
+
// confusing /model into looking like a "session-only" switch.
|
|
117
|
+
//
|
|
118
|
+
// The `aiden model` CLI subcommand (aidenCLI.ts:1773-1777) has
|
|
119
|
+
// always persisted; this brings the REPL `/model` path in line.
|
|
120
|
+
// Best-effort: if `ctx.config` isn't plumbed (test harness, etc.)
|
|
121
|
+
// we still succeeded for the live session — emit a subtle warning
|
|
122
|
+
// instead of failing the whole switch.
|
|
123
|
+
let persisted = false;
|
|
124
|
+
if (ctx.config) {
|
|
125
|
+
try {
|
|
126
|
+
ctx.config.set('model.provider', providerId);
|
|
127
|
+
ctx.config.set('model.modelId', modelId);
|
|
128
|
+
await ctx.config.save();
|
|
129
|
+
persisted = true;
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
ctx.display.warn(`Switched the live session but could not persist to config.yaml: ` +
|
|
133
|
+
`${err.message}. Next boot may revert.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
ctx.display.success(persisted
|
|
137
|
+
? `Now using ${providerId}:${modelId} (saved to config.yaml)`
|
|
138
|
+
: `Now using ${providerId}:${modelId} (session only — not persisted)`);
|
|
110
139
|
return {};
|
|
111
140
|
},
|
|
112
141
|
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* cli/v4/display/capabilityCard.ts — Aiden v4.1.3-essentials.
|
|
10
|
+
*
|
|
11
|
+
* Renders the structured "capability card" tools return on certain
|
|
12
|
+
* failure classes:
|
|
13
|
+
* 1. Platform unsupported (e.g. media_transport called on Linux)
|
|
14
|
+
* 2. Auth missing (provider 401/403, or a tool that needs a specific
|
|
15
|
+
* OAuth/API key)
|
|
16
|
+
*
|
|
17
|
+
* Distinct from the one-line tool-trail row (`display.toolRow`) because
|
|
18
|
+
* it's a different category of information — a state assessment of
|
|
19
|
+
* what the user CAN still do versus what they CANNOT, with a one-line
|
|
20
|
+
* fix hint. Rendered as a box-bordered multi-line block.
|
|
21
|
+
*
|
|
22
|
+
* Pure module — takes the data + a colorize callback, returns lines.
|
|
23
|
+
* No I/O, no SkinEngine reach-through. Caller writes the result.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.renderCapabilityCard = renderCapabilityCard;
|
|
27
|
+
exports.truncToContent = truncToContent;
|
|
28
|
+
const box_1 = require("../box");
|
|
29
|
+
/** Total card width (chars). Wide enough for typical action labels;
|
|
30
|
+
* short enough to stay readable on narrow terminals. */
|
|
31
|
+
const CARD_WIDTH = 64;
|
|
32
|
+
/** Box-content width = CARD_WIDTH minus 2 border chars and 2 padding. */
|
|
33
|
+
const CONTENT_WIDTH = CARD_WIDTH - 4;
|
|
34
|
+
/**
|
|
35
|
+
* Render a capability card from `data`. Returns an array of lines
|
|
36
|
+
* (no trailing newlines). Caller writes them with appended `\n`:
|
|
37
|
+
*
|
|
38
|
+
* for (const line of renderCapabilityCard(data, colorize)) {
|
|
39
|
+
* display.write(line + '\n');
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* Layout:
|
|
43
|
+
*
|
|
44
|
+
* ┌── ⚠ <title> ──────────────────────────────┐
|
|
45
|
+
* │ │
|
|
46
|
+
* │ Can still: │
|
|
47
|
+
* │ ✓ <action 1> │
|
|
48
|
+
* │ ✓ <action 2> │
|
|
49
|
+
* │ │
|
|
50
|
+
* │ Cannot reliably: │
|
|
51
|
+
* │ ✗ <action 1> │
|
|
52
|
+
* │ ✗ <action 2> │
|
|
53
|
+
* │ │
|
|
54
|
+
* │ Fix: <one-line guidance> │
|
|
55
|
+
* └───────────────────────────────────────────┘
|
|
56
|
+
*
|
|
57
|
+
* Empty `canStill` or `cannotReliably` arrays cause the corresponding
|
|
58
|
+
* section to be omitted (no empty heading). Always renders at least
|
|
59
|
+
* the title + fix line so the user has actionable signal.
|
|
60
|
+
*/
|
|
61
|
+
function renderCapabilityCard(data, colorize) {
|
|
62
|
+
// Pre-color the section headings + bullet markers.
|
|
63
|
+
const heading = (s) => colorize(s, 'warn');
|
|
64
|
+
const okMark = colorize('✓', 'success');
|
|
65
|
+
const noMark = colorize('✗', 'error');
|
|
66
|
+
const fixLbl = colorize('Fix:', 'tool');
|
|
67
|
+
// Compose the inner rows that boxSharp will wrap. Each row is the
|
|
68
|
+
// CONTENT (no border) — boxSharp adds the side borders + padding.
|
|
69
|
+
const rows = [];
|
|
70
|
+
if (data.canStill.length > 0) {
|
|
71
|
+
rows.push('');
|
|
72
|
+
rows.push(heading('Can still:'));
|
|
73
|
+
for (const action of data.canStill) {
|
|
74
|
+
rows.push(` ${okMark} ${truncToContent(action)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (data.cannotReliably.length > 0) {
|
|
78
|
+
rows.push('');
|
|
79
|
+
rows.push(heading('Cannot reliably:'));
|
|
80
|
+
for (const action of data.cannotReliably) {
|
|
81
|
+
rows.push(` ${noMark} ${truncToContent(action)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
rows.push('');
|
|
85
|
+
// Fix line — split-wrap to two lines if needed so long guidance
|
|
86
|
+
// doesn't get cut off mid-sentence by boxSharp's clipper.
|
|
87
|
+
const fixText = data.fix;
|
|
88
|
+
const fixPrefix = 'Fix: ';
|
|
89
|
+
const fixPrefixVis = (0, box_1.visibleLength)(fixPrefix);
|
|
90
|
+
if ((0, box_1.visibleLength)(fixText) + fixPrefixVis <= CONTENT_WIDTH) {
|
|
91
|
+
rows.push(`${fixLbl} ${fixText}`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
rows.push(fixLbl);
|
|
95
|
+
// Wrap the fix text across content-width lines.
|
|
96
|
+
let remaining = fixText;
|
|
97
|
+
const wrapLimit = CONTENT_WIDTH - 2; // 2-space indent
|
|
98
|
+
while (remaining.length > 0) {
|
|
99
|
+
const chunk = remaining.length <= wrapLimit
|
|
100
|
+
? remaining
|
|
101
|
+
: breakAtWord(remaining, wrapLimit);
|
|
102
|
+
rows.push(` ${chunk}`);
|
|
103
|
+
remaining = remaining.slice(chunk.length).trimStart();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
rows.push('');
|
|
107
|
+
// Title is rendered into the top border by boxSharp. Prefix with
|
|
108
|
+
// a warning glyph (yellow) so the user reads it as an attention card.
|
|
109
|
+
const title = `${colorize('⚠', 'warn')} ${data.title}`;
|
|
110
|
+
return (0, box_1.boxSharp)(rows, CARD_WIDTH, title).split('\n');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Shorten an action label so it fits the bullet column with room for
|
|
114
|
+
* the marker (" ✓ ") and the border padding. Appends an ellipsis when
|
|
115
|
+
* truncated. Pure — exported for unit tests.
|
|
116
|
+
*/
|
|
117
|
+
function truncToContent(s) {
|
|
118
|
+
// Reserve 6 chars for " ✓ " prefix + 2 for border padding margin.
|
|
119
|
+
const cap = CONTENT_WIDTH - 6;
|
|
120
|
+
if ((0, box_1.visibleLength)(s) <= cap)
|
|
121
|
+
return s;
|
|
122
|
+
return s.slice(0, cap - 1) + '…';
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Break `s` at the last word boundary at-or-before `limit`. Falls back
|
|
126
|
+
* to a hard cut at `limit` when no whitespace appears in the prefix.
|
|
127
|
+
* Pure helper used by the Fix-line wrapper.
|
|
128
|
+
*/
|
|
129
|
+
function breakAtWord(s, limit) {
|
|
130
|
+
if (s.length <= limit)
|
|
131
|
+
return s;
|
|
132
|
+
const slice = s.slice(0, limit);
|
|
133
|
+
const lastSpace = slice.lastIndexOf(' ');
|
|
134
|
+
return lastSpace > 0 ? slice.slice(0, lastSpace) : slice;
|
|
135
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* cli/v4/display/sessionEndCard.ts — Aiden v4.1.3-repl-polish
|
|
10
|
+
*
|
|
11
|
+
* Renders a compact session-end summary card from a SessionDistillation.
|
|
12
|
+
*
|
|
13
|
+
* Returned as an array of plain lines (WITHOUT trailing '\n'). The caller
|
|
14
|
+
* writes them with a newline appended, e.g.:
|
|
15
|
+
*
|
|
16
|
+
* for (const line of renderSessionEndCard(dist, colorize)) {
|
|
17
|
+
* display.write(line + '\n');
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Design rules (from spec):
|
|
21
|
+
* - Skip entirely when user_turns === 0 (silent/internal sessions).
|
|
22
|
+
* - Label column is colon-aligned to column LABEL_COL (14).
|
|
23
|
+
* - Session ID rendered in 'session' color (soft cyan).
|
|
24
|
+
* - Bullets / decisions / open_items shown only when non-empty.
|
|
25
|
+
* - Takes a `colorize` callback instead of a SkinEngine directly, so
|
|
26
|
+
* the function is fully unit-testable without a Display stack.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.renderSessionEndCard = renderSessionEndCard;
|
|
30
|
+
/** Width of the "Label:" prefix, colon included, padded to this column. */
|
|
31
|
+
const LABEL_COL = 14;
|
|
32
|
+
/** Horizontal rule width (chars). */
|
|
33
|
+
const HR_WIDTH = 48;
|
|
34
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
35
|
+
function labelRow(label, value) {
|
|
36
|
+
return `${`${label}:`.padEnd(LABEL_COL)}${value}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Format a wall-clock duration from two ISO timestamps.
|
|
40
|
+
* Returns '—' when the delta is ≤0 or non-finite (e.g. partial distillation).
|
|
41
|
+
*/
|
|
42
|
+
function fmtDuration(startIso, endIso) {
|
|
43
|
+
const ms = new Date(endIso).getTime() - new Date(startIso).getTime();
|
|
44
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
45
|
+
return '—';
|
|
46
|
+
const sec = ms / 1000;
|
|
47
|
+
if (sec < 60)
|
|
48
|
+
return `${Math.round(sec)}s`;
|
|
49
|
+
const mins = Math.floor(sec / 60);
|
|
50
|
+
const remSec = Math.round(sec - mins * 60);
|
|
51
|
+
return remSec > 0 ? `${mins}m ${remSec}s` : `${mins}m`;
|
|
52
|
+
}
|
|
53
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Render a session-end card from `dist`.
|
|
56
|
+
*
|
|
57
|
+
* @param dist Completed SessionDistillation (may be partial).
|
|
58
|
+
* @param colorize Skin-aware colorizer — `(text, kind) => coloredText`.
|
|
59
|
+
* @param distillationPath Absolute path the distillation JSON was written to,
|
|
60
|
+
* if any. Rendered as a `Distillation:` row so the
|
|
61
|
+
* user has something concrete to inspect / pass to
|
|
62
|
+
* recall_session. Omitted from the card when null /
|
|
63
|
+
* undefined (e.g. write failed earlier).
|
|
64
|
+
* @returns Array of lines (no trailing newlines). Empty when
|
|
65
|
+
* `user_turns === 0`.
|
|
66
|
+
*/
|
|
67
|
+
function renderSessionEndCard(dist, colorize, distillationPath) {
|
|
68
|
+
if (dist.user_turns === 0)
|
|
69
|
+
return [];
|
|
70
|
+
const lines = [];
|
|
71
|
+
const hr = colorize('─'.repeat(HR_WIDTH), 'muted');
|
|
72
|
+
const bullet = colorize('•', 'muted');
|
|
73
|
+
// ── Header block ───────────────────────────────────────────────────────
|
|
74
|
+
lines.push(hr);
|
|
75
|
+
lines.push(labelRow('Session', colorize(dist.session_id, 'session')));
|
|
76
|
+
lines.push(labelRow('Duration', fmtDuration(dist.started_at, dist.ended_at)));
|
|
77
|
+
lines.push(labelRow('Turns', String(dist.user_turns)));
|
|
78
|
+
lines.push(labelRow('Exit', dist.exit_path));
|
|
79
|
+
if (dist.files_touched.length > 0) {
|
|
80
|
+
// Show at most 6 files; truncate list with '…' if longer.
|
|
81
|
+
const shown = dist.files_touched.slice(0, 6);
|
|
82
|
+
const suffix = dist.files_touched.length > 6
|
|
83
|
+
? ` … +${dist.files_touched.length - 6} more`
|
|
84
|
+
: '';
|
|
85
|
+
lines.push(labelRow('Files', shown.join(', ') + suffix));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
lines.push(labelRow('Files', colorize('(none)', 'muted')));
|
|
89
|
+
}
|
|
90
|
+
if (dist.tools_used.length > 0) {
|
|
91
|
+
const top = [...dist.tools_used]
|
|
92
|
+
.sort((a, b) => b.count - a.count)
|
|
93
|
+
.slice(0, 5)
|
|
94
|
+
.map(t => `${t.name}(${t.count})`)
|
|
95
|
+
.join(', ');
|
|
96
|
+
lines.push(labelRow('Tools', top));
|
|
97
|
+
}
|
|
98
|
+
if (distillationPath) {
|
|
99
|
+
lines.push(labelRow('Distillation', colorize(distillationPath, 'muted')));
|
|
100
|
+
}
|
|
101
|
+
lines.push(hr);
|
|
102
|
+
// ── Semantic sections (LLM-generated, may be empty on partial) ─────────
|
|
103
|
+
if (dist.bullets.length > 0) {
|
|
104
|
+
lines.push('');
|
|
105
|
+
lines.push(colorize('What happened:', 'heading'));
|
|
106
|
+
for (const b of dist.bullets) {
|
|
107
|
+
lines.push(` ${bullet} ${b}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (dist.decisions.length > 0) {
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push(colorize('Decisions:', 'heading'));
|
|
113
|
+
for (const d of dist.decisions) {
|
|
114
|
+
lines.push(` ${bullet} ${d}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (dist.open_items.length > 0) {
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push(colorize('Open items:', 'heading'));
|
|
120
|
+
for (const o of dist.open_items) {
|
|
121
|
+
lines.push(` ${bullet} ${o}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Blank line so "Goodbye." has breathing room.
|
|
125
|
+
lines.push('');
|
|
126
|
+
return lines;
|
|
127
|
+
}
|