@dotdrelle/wiki-manager 0.15.60 → 0.15.62
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/package.json +1 -1
- package/src/core/buildInfo.json +2 -2
- package/src/core/mcp.js +1 -1
- package/src/core/skillCompiler.test.js +1 -1
- package/src/runtime/controlMessages.js +1 -0
- package/src/runtime/server.js +117 -15
- package/src/runtime/server.test.js +11 -7
- package/src/runtime/skillChain.e2e.test.js +1 -1
- package/src/shell/LeftPane.tsx +1 -1
- package/src/shell/RightPane.tsx +11 -11
package/package.json
CHANGED
package/src/core/buildInfo.json
CHANGED
package/src/core/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
-
const WIKI_MANAGER_VERSION = '0.15.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.15.62';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
|
@@ -31,7 +31,7 @@ test('validation rejects technical routing details', () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test('scaffold skills preserve existing capabilities and split only wiki-sync', async () => {
|
|
34
|
-
const expected = { pipeline: 1, 'wiki-ingest':
|
|
34
|
+
const expected = { pipeline: 1, 'wiki-ingest': 2, 'wiki-build': 1, deliver: 1, diagnose: 1, status: 1, 'new-template': 1, 'wiki-sync': 2 };
|
|
35
35
|
for (const [name, count] of Object.entries(expected)) {
|
|
36
36
|
const raw = readFileSync(resolve('../llm-wiki/scaffold/workspace/.wiki/skills', `${name}.md`), 'utf8');
|
|
37
37
|
const { meta, body } = parseFrontmatter(raw);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const CONTROL_MESSAGES = {
|
|
15
15
|
queued_for_future_run: 'Request added to the queue — it will start automatically after the current run.',
|
|
16
|
+
control_run_started: 'A queued request is now starting.',
|
|
16
17
|
plan_patch_proposed: 'Plan patch proposed. Approve it explicitly to apply it to the active plan.',
|
|
17
18
|
ambiguous_control: 'A run is already active, and this looks like a new action. Say "queue it" to run it after the current run, "modify the run" to change the active plan, "cancel" to stop the current run first — or wait for it to finish.',
|
|
18
19
|
converse_while_running: 'Runtime run is still active. This message was treated as conversation and did not create a queued run.',
|
package/src/runtime/server.js
CHANGED
|
@@ -12,6 +12,7 @@ import { matchSkillInvocation } from '../core/skillInvocation.js';
|
|
|
12
12
|
import { reconcileControlQueue } from './controlDrain.js';
|
|
13
13
|
import { cancelControlChain, cancelQueuedControlItem } from './controlCancellation.js';
|
|
14
14
|
import { generateSkillAcknowledgment, runSkillChain } from './skillRun.js';
|
|
15
|
+
import { emitRuntimeLog } from './supervisor.js';
|
|
15
16
|
import { findSkill, listSkills } from '../core/skills.js';
|
|
16
17
|
|
|
17
18
|
const PRIVATE_CONTROL_INPUTS = new WeakMap();
|
|
@@ -780,7 +781,7 @@ export function startRuntimeServer({
|
|
|
780
781
|
return { killed: true, workspace: targetWorkspace, runId: targetRunId, runs, tasks, queued, ...(purged !== null ? { purged } : {}) };
|
|
781
782
|
}
|
|
782
783
|
|
|
783
|
-
function startRuntimeRun(context, body, { controlItemId = null, waitForPlan = false } = {}) {
|
|
784
|
+
function startRuntimeRun(context, body, { controlItemId = null, waitForPlan = false, announceLaunch = false } = {}) {
|
|
784
785
|
const runId = randomUUID();
|
|
785
786
|
const runWorkspace = context.workspace ?? body.workspace ?? null;
|
|
786
787
|
context.running = true;
|
|
@@ -803,6 +804,14 @@ export function startRuntimeServer({
|
|
|
803
804
|
workspace: runWorkspace,
|
|
804
805
|
payload: { id: controlItemId, runId },
|
|
805
806
|
}));
|
|
807
|
+
// A control item is now a live run: announce it in the conversation so the
|
|
808
|
+
// "queued" acknowledgement is closed out by a "starting" one. A task is a
|
|
809
|
+
// task whether it waited or not — it is about to go through approval — so
|
|
810
|
+
// this is not gated on having waited. Skill-chain steps are skipped: they
|
|
811
|
+
// already announced the whole skill at invocation.
|
|
812
|
+
if (announceLaunch) {
|
|
813
|
+
announceControlLaunch(context.session, body.publicInput ?? body.input, runWorkspace);
|
|
814
|
+
}
|
|
806
815
|
}
|
|
807
816
|
const runPromise = run(context, runBody, { signal: context.currentAbortController.signal, runId });
|
|
808
817
|
runPromise
|
|
@@ -860,7 +869,7 @@ export function startRuntimeServer({
|
|
|
860
869
|
},
|
|
861
870
|
}
|
|
862
871
|
: {}),
|
|
863
|
-
}, { controlItemId: item.id }),
|
|
872
|
+
}, { controlItemId: item.id, announceLaunch: !item.chainId }),
|
|
864
873
|
skipItem: (item, reason) => {
|
|
865
874
|
privateControlInputsFor(context.session).delete(item.id);
|
|
866
875
|
emitControlSkipped(context, item, reason);
|
|
@@ -1091,7 +1100,11 @@ export function approvalRequestFromStatus(status) {
|
|
|
1091
1100
|
|
|
1092
1101
|
async function handleControlMessage(context, store, input, { intent = null, startNextControlRequest = () => false, cancel = null, approve = null } = {}) {
|
|
1093
1102
|
const status = controlStatus(context, store);
|
|
1094
|
-
const classification = classifyControlMessage(input, status,
|
|
1103
|
+
const classification = await classifyControlMessage(input, status, {
|
|
1104
|
+
forcedIntent: intent,
|
|
1105
|
+
llm: context?.session?.llm,
|
|
1106
|
+
session: context?.session,
|
|
1107
|
+
});
|
|
1095
1108
|
if (classification.kind === 'observe') {
|
|
1096
1109
|
return readOnlyControlResponse('observe', classification, status, explainControlState(status));
|
|
1097
1110
|
}
|
|
@@ -1129,6 +1142,7 @@ async function handleControlMessage(context, store, input, { intent = null, star
|
|
|
1129
1142
|
// startNextControlRequest), which can change running/plan/status — a full
|
|
1130
1143
|
// controlStatus() recompute is required here, not just controlQueue.
|
|
1131
1144
|
void startNextControlRequest(context);
|
|
1145
|
+
const explanation = await generateControlAcknowledgment(context?.session, { kind: 'queued', input });
|
|
1132
1146
|
return {
|
|
1133
1147
|
statusCode: 202,
|
|
1134
1148
|
body: {
|
|
@@ -1137,7 +1151,7 @@ async function handleControlMessage(context, store, input, { intent = null, star
|
|
|
1137
1151
|
classification,
|
|
1138
1152
|
item,
|
|
1139
1153
|
...controlStatus(context, store),
|
|
1140
|
-
explanation
|
|
1154
|
+
explanation,
|
|
1141
1155
|
},
|
|
1142
1156
|
};
|
|
1143
1157
|
}
|
|
@@ -1158,6 +1172,61 @@ async function handleControlMessage(context, store, input, { intent = null, star
|
|
|
1158
1172
|
: controlMessage(context?.session, 'converse_while_idle'));
|
|
1159
1173
|
}
|
|
1160
1174
|
|
|
1175
|
+
/*
|
|
1176
|
+
Control-lane acknowledgements are Donna's to localize.
|
|
1177
|
+
|
|
1178
|
+
The control lane stays deterministic in its CLASSIFICATION and its actions,
|
|
1179
|
+
but the acknowledgement the user reads ("queued, will run after this one" /
|
|
1180
|
+
"the queued task is starting") is a conversational reply: it goes through a
|
|
1181
|
+
single bounded LLM completion, like the skill-launch acknowledgement, and
|
|
1182
|
+
falls back to the deterministic English catalog when no LLM is configured or
|
|
1183
|
+
the call fails. The fallback is what keeps the lane deterministic-under-failure.
|
|
1184
|
+
*/
|
|
1185
|
+
async function generateControlAcknowledgment(session, { kind, input }) {
|
|
1186
|
+
const language = String(session?.language ?? '').trim().toLowerCase() || 'en';
|
|
1187
|
+
const llm = session?.llm;
|
|
1188
|
+
const fallback = kind === 'queued'
|
|
1189
|
+
? controlMessage(session, 'queued_for_future_run')
|
|
1190
|
+
: controlMessage(session, 'control_run_started');
|
|
1191
|
+
if (llm && typeof llm.complete === 'function') {
|
|
1192
|
+
try {
|
|
1193
|
+
const scenario = kind === 'queued'
|
|
1194
|
+
? 'The user requested a new task while a run is active. It was queued and will start automatically after the current run finishes.'
|
|
1195
|
+
: 'A task the user queued earlier is now starting.';
|
|
1196
|
+
const instruction = kind === 'queued'
|
|
1197
|
+
? 'their request is queued and will run after the current run finishes'
|
|
1198
|
+
: 'the queued task is now starting';
|
|
1199
|
+
const reply = await llm.complete({
|
|
1200
|
+
system: 'You are Donna, the workspace assistant. You acknowledge a runtime queue event in the user\'s language. Be concise: exactly one short sentence.',
|
|
1201
|
+
input: `${scenario}\n\nThe task is: ${input}\n\nWrite ONE short sentence in ${language} that tells the user ${instruction}. Return only that sentence, nothing else.`,
|
|
1202
|
+
signal: AbortSignal.timeout(8_000),
|
|
1203
|
+
});
|
|
1204
|
+
const text = String(reply ?? '').trim();
|
|
1205
|
+
if (text) return text;
|
|
1206
|
+
emitRuntimeLog(session, 'control-acknowledgment: LLM returned an empty reply, using the deterministic fallback');
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
// A degradation must announce itself: silently falling through here
|
|
1209
|
+
// hides the difference between "no LLM configured" (expected) and "the
|
|
1210
|
+
// configured LLM is failing every call" (a real problem) — both would
|
|
1211
|
+
// otherwise look identical from the Shell or serve UI.
|
|
1212
|
+
emitRuntimeLog(session, `control-acknowledgment: LLM call failed, using the deterministic fallback — ${err instanceof Error ? err.message : String(err)}`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return fallback;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function announceControlLaunch(session, input, workspace) {
|
|
1219
|
+
void generateControlAcknowledgment(session, { kind: 'started', input })
|
|
1220
|
+
.then((content) => {
|
|
1221
|
+
dispatchAgentEvent(session, createAgentEvent('assistant_message', {
|
|
1222
|
+
origin: 'runtime',
|
|
1223
|
+
workspace,
|
|
1224
|
+
payload: { content, independent: true },
|
|
1225
|
+
}));
|
|
1226
|
+
})
|
|
1227
|
+
.catch(() => {});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1161
1230
|
/*
|
|
1162
1231
|
`skillStack` accompagne l'élément, il ne vit pas sur la session.
|
|
1163
1232
|
|
|
@@ -1353,13 +1422,15 @@ function rejectPlanPatch(context, store, patchId, reason) {
|
|
|
1353
1422
|
};
|
|
1354
1423
|
}
|
|
1355
1424
|
|
|
1356
|
-
//
|
|
1357
|
-
//
|
|
1358
|
-
//
|
|
1359
|
-
//
|
|
1360
|
-
//
|
|
1361
|
-
//
|
|
1362
|
-
|
|
1425
|
+
// Classifier for control §4.2 of the plan directeur. The plan expects an
|
|
1426
|
+
// LLM-backed classification — "the classification LLM se trompera" — and this
|
|
1427
|
+
// is that, now: the only deterministic matches left are the runtime's own
|
|
1428
|
+
// control verbs (cancel, an explicit "later/queue", status and plan-change
|
|
1429
|
+
// wording). Deciding "is this a NEW task to queue vs plain conversation" is a
|
|
1430
|
+
// semantic judgement about the workspace's domain, so it is never a keyword
|
|
1431
|
+
// list here — it goes to the model, bounded, and falls back to the choice menu
|
|
1432
|
+
// (`ambiguous`) rather than guessing when no model is available.
|
|
1433
|
+
async function classifyControlMessage(input, status, { forcedIntent = null, llm = null, session = null } = {}) {
|
|
1363
1434
|
// Caller (the /control message route) already trims and rejects empty input.
|
|
1364
1435
|
const lower = String(input ?? '').toLowerCase();
|
|
1365
1436
|
const intent = forcedIntent ? String(forcedIntent).toLowerCase() : null;
|
|
@@ -1377,22 +1448,53 @@ function classifyControlMessage(input, status, forcedIntent = null) {
|
|
|
1377
1448
|
if (explicit) {
|
|
1378
1449
|
return { kind: explicit, confidence: 1, reason: 'explicit_intent' };
|
|
1379
1450
|
}
|
|
1451
|
+
// Cancel stays a keyword: it is a runtime control verb, and an abort must not
|
|
1452
|
+
// wait on a model round-trip.
|
|
1380
1453
|
if (/\b(cancel|annule|stop|arr[eê]te|interromps|abort)\b/i.test(lower)) {
|
|
1381
1454
|
return { kind: 'cancel', confidence: 0.86, reason: 'cancel_request' };
|
|
1382
1455
|
}
|
|
1383
1456
|
if (/\b(plus tard|later|ensuite|apr[eè]s ce run|enqueue|mets en file|met en file|futur|next run|future run)\b/i.test(lower)) {
|
|
1384
1457
|
return { kind: 'enqueue_run', confidence: 0.8, reason: 'future_run_request' };
|
|
1385
1458
|
}
|
|
1386
|
-
if (/\b(o[uù] en es[t-]|status|statut|progress|progression|
|
|
1459
|
+
if (/\b(o[uù] en es[t-]|status|statut|progress|progression|logs?|explique|explain|inspect|show|montre|quoi de neuf)\b/i.test(lower)) {
|
|
1387
1460
|
return { kind: 'observe', confidence: 0.86, reason: 'status_or_explanation_request' };
|
|
1388
1461
|
}
|
|
1389
1462
|
if (status.running && /\b(ajoute|add|change|modifie|modify|remplace|replace|retire|remove|skip|ignore|apr[eè]s|before|after|chaque|each|plan|step|t[aâ]che)\b/i.test(lower)) {
|
|
1390
1463
|
return { kind: 'modify_run', confidence: 0.78, reason: 'active_run_change_request' };
|
|
1391
1464
|
}
|
|
1392
|
-
if (status.running
|
|
1393
|
-
|
|
1465
|
+
if (!status.running) return { kind: 'converse', confidence: 0.62, reason: 'plain_conversation' };
|
|
1466
|
+
// A run is active and none of the runtime control verbs matched. The message
|
|
1467
|
+
// is either a request to perform a NEW mutating task (→ queue it to run
|
|
1468
|
+
// after the current one) or ordinary conversation — that is a judgement about
|
|
1469
|
+
// the workspace's domain, so the model decides it, never a keyword list.
|
|
1470
|
+
if (llm && typeof llm.complete === 'function') {
|
|
1471
|
+
try {
|
|
1472
|
+
const reply = await llm.complete({
|
|
1473
|
+
system: 'You classify one user message typed while a run is already active. Return exactly one word, nothing else.',
|
|
1474
|
+
input: [
|
|
1475
|
+
`The user typed this while a run is active: "${input}"`,
|
|
1476
|
+
'',
|
|
1477
|
+
'Choose ONE of:',
|
|
1478
|
+
'- "action" — a request to perform a NEW task (generate, create, ingest, build, export, convert, send, publish, produce…), which must run after the current run.',
|
|
1479
|
+
'- "conversation" — ordinary conversation, a question, or an unrelated remark.',
|
|
1480
|
+
'',
|
|
1481
|
+
'Return only that one word.',
|
|
1482
|
+
].join('\n'),
|
|
1483
|
+
signal: AbortSignal.timeout(8_000),
|
|
1484
|
+
});
|
|
1485
|
+
const kind = String(reply ?? '').trim().toLowerCase();
|
|
1486
|
+
if (kind.startsWith('action')) return { kind: 'enqueue_run', confidence: 0.85, reason: 'llm_classified_action' };
|
|
1487
|
+
if (kind.startsWith('conversation')) return { kind: 'converse', confidence: 0.85, reason: 'llm_classified_conversation' };
|
|
1488
|
+
emitRuntimeLog(session, `control-classify: LLM returned an unrecognized reply, falling back to the choice menu — ${JSON.stringify(kind).slice(0, 200)}`);
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
// A degradation must announce itself: silently falling through here
|
|
1491
|
+
// hides the difference between "no LLM configured" (expected) and "the
|
|
1492
|
+
// configured LLM is failing every call" (a real problem) — both would
|
|
1493
|
+
// otherwise look identical from the Shell or serve UI.
|
|
1494
|
+
emitRuntimeLog(session, `control-classify: LLM call failed, falling back to the choice menu — ${err instanceof Error ? err.message : String(err)}`);
|
|
1495
|
+
}
|
|
1394
1496
|
}
|
|
1395
|
-
return { kind: '
|
|
1497
|
+
return { kind: 'ambiguous', confidence: 0.45, reason: 'action_vs_conversation_unclear' };
|
|
1396
1498
|
}
|
|
1397
1499
|
|
|
1398
1500
|
function isAuthorized(request, token) {
|
|
@@ -1434,10 +1434,14 @@ test('runtime server control message records active plan mutation as a proposal'
|
|
|
1434
1434
|
}
|
|
1435
1435
|
});
|
|
1436
1436
|
|
|
1437
|
-
test('runtime server
|
|
1437
|
+
test('runtime server auto-queues a clear new action while a run is active', async (t) => {
|
|
1438
1438
|
const session = {
|
|
1439
1439
|
workspace: 'acme',
|
|
1440
1440
|
controlQueue: [],
|
|
1441
|
+
_onAgentEvent: () => {},
|
|
1442
|
+
// The classifier asks the model whether the message is a new action; a
|
|
1443
|
+
// keyword list is deliberately not part of the code path.
|
|
1444
|
+
llm: { complete: async () => 'action' },
|
|
1441
1445
|
};
|
|
1442
1446
|
let runCount = 0;
|
|
1443
1447
|
let handle;
|
|
@@ -1451,6 +1455,7 @@ test('runtime server control message reports ambiguity without starting a run',
|
|
|
1451
1455
|
status: 'running',
|
|
1452
1456
|
plan: [{ step: 1, description: 'Generate', status: 'running' }],
|
|
1453
1457
|
queue: [],
|
|
1458
|
+
controlQueue: session.controlQueue,
|
|
1454
1459
|
approvals: [],
|
|
1455
1460
|
summary: null,
|
|
1456
1461
|
}),
|
|
@@ -1478,12 +1483,11 @@ test('runtime server control message reports ambiguity without starting a run',
|
|
|
1478
1483
|
headers: { 'Content-Type': 'application/json' },
|
|
1479
1484
|
body: JSON.stringify({ action: 'message', input: 'Lance aussi la publication' }),
|
|
1480
1485
|
});
|
|
1481
|
-
assert.equal(response.status,
|
|
1486
|
+
assert.equal(response.status, 202);
|
|
1482
1487
|
const body = await response.json();
|
|
1483
|
-
assert.equal(body.kind, '
|
|
1484
|
-
assert.equal(body.
|
|
1485
|
-
assert.equal(
|
|
1486
|
-
assert.equal(runCount, 0);
|
|
1488
|
+
assert.equal(body.kind, 'enqueue_run');
|
|
1489
|
+
assert.equal(body.item.status, 'queued');
|
|
1490
|
+
assert.equal(runCount, 0, 'the queued task must not start while the current run is active');
|
|
1487
1491
|
} finally {
|
|
1488
1492
|
await handle.close();
|
|
1489
1493
|
}
|
|
@@ -1545,7 +1549,7 @@ test('runtime server drains queued control requests when idle', async (t) => {
|
|
|
1545
1549
|
assert.match(receivedBody.runId, /^[0-9a-f-]{36}$/);
|
|
1546
1550
|
assert.equal(session.controlQueue[0].status, 'running');
|
|
1547
1551
|
assert.equal(session.controlQueue[0].runId, receivedBody.runId);
|
|
1548
|
-
assert.deepEqual(events.map((event) => event.type), ['control_enqueued', 'control_started']);
|
|
1552
|
+
assert.deepEqual(events.map((event) => event.type), ['control_enqueued', 'control_started', 'assistant_message']);
|
|
1549
1553
|
} finally {
|
|
1550
1554
|
await handle.close();
|
|
1551
1555
|
}
|
|
@@ -202,7 +202,7 @@ test('E2E-003 cancel: the running step and its chain stop, unrelated queue survi
|
|
|
202
202
|
// that silently fragments would show up as extra runs, not as extra objectives.
|
|
203
203
|
const PERFORMANCE_TABLE = {
|
|
204
204
|
pipeline: 1,
|
|
205
|
-
'wiki-ingest':
|
|
205
|
+
'wiki-ingest': 2,
|
|
206
206
|
'wiki-build': 1,
|
|
207
207
|
deliver: 1,
|
|
208
208
|
diagnose: 1,
|
package/src/shell/LeftPane.tsx
CHANGED
|
@@ -859,7 +859,7 @@ export function LeftPane(props: {
|
|
|
859
859
|
above the composer keeps the current job in view while composing; the
|
|
860
860
|
right pane keeps the full Plan/Queue/Logs detail.
|
|
861
861
|
*/}
|
|
862
|
-
<box flexShrink={0} height={4} flexDirection="column" overflow="hidden">
|
|
862
|
+
<box flexShrink={0} height={4} flexDirection="column" overflow="hidden" backgroundColor="#111318">
|
|
863
863
|
<ActivityPanel activities={props.activities} width={props.width - 2} />
|
|
864
864
|
</box>
|
|
865
865
|
<ChatInput
|
package/src/shell/RightPane.tsx
CHANGED
|
@@ -20,7 +20,10 @@ type LogLineParts = { time: string | null; message: string };
|
|
|
20
20
|
// 4 slots (was 6): items can now span up to 5 lines each (wrapped label +
|
|
21
21
|
// wrapped status/error), so fewer, readable entries beat more, truncated ones.
|
|
22
22
|
const ACTIVITY_SLOTS = Array.from({ length: 4 }, (_, index) => index);
|
|
23
|
-
|
|
23
|
+
// Hauteur du panneau Plan : 6 lignes visibles au maximum. Les etapes suivantes
|
|
24
|
+
// restent atteignables en faisant defiler la scrollbox (barre de defilement
|
|
25
|
+
// affichee des que le plan depasse la fenetre).
|
|
26
|
+
const PLAN_VIEWPORT_ROWS = 6;
|
|
24
27
|
|
|
25
28
|
function wrapLine(value: string, width: number) {
|
|
26
29
|
const max = Math.max(8, width);
|
|
@@ -194,12 +197,10 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
|
|
|
194
197
|
// Keep one column for the native vertical scrollbar when the plan is long.
|
|
195
198
|
const lineWidth = () => Math.max(8, props.width - 3);
|
|
196
199
|
const firstPending = () => props.plan.find((s) => s.status === 'pending')?.step ?? null;
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
const isRunningStep = (step: PlanStep) => String(step.status ?? '').toLowerCase() === 'running';
|
|
202
|
-
const stepTextWidth = (step: PlanStep) => lineWidth() - (isRunningStep(step) ? 1 : 0);
|
|
200
|
+
// Plus de liseré bleu à gauche : l'icône et la couleur du texte suffisent à
|
|
201
|
+
// distinguer une étape en cours, et le cadre se voyait aussi sur une étape en
|
|
202
|
+
// attente (jaune). Toutes les étapes utilisent donc la même largeur.
|
|
203
|
+
const stepTextWidth = (_step: PlanStep) => lineWidth();
|
|
203
204
|
const icon = (rawStatus: string) => {
|
|
204
205
|
const status = String(rawStatus ?? '').toLowerCase();
|
|
205
206
|
if (DONE_STATUSES.includes(status)) return '[✓]';
|
|
@@ -241,11 +242,10 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
|
|
|
241
242
|
{(step) => {
|
|
242
243
|
// Wrap step descriptions over up to 2 lines instead of truncating —
|
|
243
244
|
// "Ingest des 39 documents raw/untrac…" hid the actual target.
|
|
244
|
-
const running = () => isRunningStep(step());
|
|
245
245
|
const textWidth = () => stepTextWidth(step());
|
|
246
246
|
const lines = () => wrapLine(`${icon(step().status)} ${step().step}. ${step().description}`, textWidth()).slice(0, 2);
|
|
247
247
|
return (
|
|
248
|
-
<box flexShrink={0} flexDirection="column"
|
|
248
|
+
<box flexShrink={0} flexDirection="column">
|
|
249
249
|
<text width={textWidth()} fg={planStepColor(step(), firstPending())} content={lines()[0]} />
|
|
250
250
|
<Show when={lines()[1]}>
|
|
251
251
|
<text width={textWidth()} fg={planStepColor(step(), firstPending())} content={` ${fit(lines()[1], Math.max(8, textWidth() - 4))}`} />
|
|
@@ -265,7 +265,7 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
|
|
|
265
265
|
const visibleSlots = () => visible().map((_activity, index) => index);
|
|
266
266
|
const activityAt = (index: number) => visible()[index] ?? null;
|
|
267
267
|
return (
|
|
268
|
-
<box flexShrink={0} flexDirection="column" paddingX={1}>
|
|
268
|
+
<box flexShrink={0} flexDirection="column" paddingX={1} backgroundColor="#111318">
|
|
269
269
|
<text width={lineWidth()} fg="#D6DEE8" content="Activity" />
|
|
270
270
|
<Show when={visible().length > 0} fallback={<text width={lineWidth()} fg="#7F8C8D" content="no active jobs" />}>
|
|
271
271
|
<Index each={visibleSlots()}>
|
|
@@ -385,7 +385,7 @@ export function LogPanel(props: { logs: string[]; width: number; filter?: string
|
|
|
385
385
|
.filter((line) => activeLogTab() === 'agent-status' ? isAgentStatus(line) : !isAgentStatus(line));
|
|
386
386
|
const allLines = createMemo(() => logRenderLines(filteredLogs(), lineWidth()));
|
|
387
387
|
return (
|
|
388
|
-
<box flexGrow={2} flexDirection="column" paddingX={1} focusable={false}>
|
|
388
|
+
<box flexGrow={2} flexDirection="column" paddingX={1} marginTop={6} focusable={false}>
|
|
389
389
|
<text width={lineWidth()} fg="#4B5563" content={'─'.repeat(lineWidth())} />
|
|
390
390
|
<box height={1} flexDirection="row">
|
|
391
391
|
<text
|