@dotdrelle/wiki-manager 0.15.50 → 0.15.53
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/README.md +20 -992
- package/docker-compose.override.example.yml +19 -0
- package/package.json +10 -2
- package/src/agent/graph.js +36 -13
- package/src/agent/graph.test.js +38 -2
- package/src/cli/wiki-manager.js +19 -1
- package/src/cli/wiki-manager.test.js +31 -0
- package/src/commands/slash.js +2 -42
- package/src/commands/slash.test.js +9 -0
- package/src/core/agentEvents.js +40 -1
- package/src/core/agentEvents.test.js +29 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/googleGrants.test.js +13 -3
- package/src/core/mcp.js +1 -1
- package/src/core/runtimeLog.js +19 -3
- package/src/core/runtimeLog.test.js +27 -1
- package/src/core/skillChainView.js +19 -14
- package/src/core/skillChainView.test.js +2 -2
- package/src/core/skillInvocation.test.js +4 -4
- package/src/runtime/runner.js +2 -1
- package/src/runtime/server.test.js +3 -3
- package/src/runtime/skillRun.test.js +10 -10
- package/src/shell/LeftPane.tsx +11 -0
- package/src/shell/RightPane.tsx +26 -9
- package/src/shell/repl.test.js +2 -2
- package/src/shell/tui.tsx +4 -1
|
@@ -16,7 +16,7 @@ const SYMBOLS = {
|
|
|
16
16
|
skipped: '–',
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
const TERMINAL = new Set(['done', 'failed', 'cancelled', 'skipped']);
|
|
19
|
+
export const TERMINAL = new Set(['done', 'failed', 'cancelled', 'skipped']);
|
|
20
20
|
|
|
21
21
|
// Objectives are whole paragraphs; a chain view needs a line. Keep the first
|
|
22
22
|
// sentence, drop the parameter block the compiler appends, and never cut a word
|
|
@@ -39,19 +39,24 @@ export function projectSkillChains(controlQueue = []) {
|
|
|
39
39
|
byChain.get(item.chainId).push(item);
|
|
40
40
|
}
|
|
41
41
|
return [...byChain.entries()].map(([chainId, chainItems]) => {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
const sorted = chainItems.slice().sort((a, b) => Number(a.chainSequence ?? 0) - Number(b.chainSequence ?? 0));
|
|
43
|
+
const total = sorted.length;
|
|
44
|
+
// Compiled objectives are private execution material and never reach the
|
|
45
|
+
// projection, so a step cannot be labelled with its intent prose. When the
|
|
46
|
+
// chain has several steps, the public input is identical for all of them —
|
|
47
|
+
// labelling them all "/skill args" is what read as six tasks with the same
|
|
48
|
+
// name. Distinguish them by position instead; the skill name already sits
|
|
49
|
+
// in the chain head.
|
|
50
|
+
const steps = sorted.map((item, index) => ({
|
|
51
|
+
id: item.id,
|
|
52
|
+
sequence: Number(item.chainSequence ?? 0),
|
|
53
|
+
label: total > 1 ? `Step ${index + 1}/${total}` : chainStepLabel(item.input),
|
|
54
|
+
status: String(item.status ?? 'queued'),
|
|
55
|
+
symbol: SYMBOLS[String(item.status ?? 'queued')] ?? '○',
|
|
56
|
+
optional: item.optional === true,
|
|
57
|
+
...(item.skipReason ? { skipReason: item.skipReason } : {}),
|
|
58
|
+
...(item.runId ? { runId: item.runId } : {}),
|
|
59
|
+
}));
|
|
55
60
|
return {
|
|
56
61
|
chainId,
|
|
57
62
|
skillName: chainItems.find((item) => item.skillName)?.skillName ?? null,
|
|
@@ -19,7 +19,7 @@ test('a chain reads as ordered steps with one short label each', () => {
|
|
|
19
19
|
assert.equal(chain.selectionKind, 'description_match');
|
|
20
20
|
assert.equal(chain.status, 'running');
|
|
21
21
|
assert.deepEqual(chain.steps.map((step) => step.symbol), ['✓', '●']);
|
|
22
|
-
assert.equal(chain.steps[0].label, '
|
|
22
|
+
assert.equal(chain.steps[0].label, 'Step 1/2');
|
|
23
23
|
assert.equal(chain.steps[1].runId, 'run-b');
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -40,7 +40,7 @@ test('after a cancel the chain shows the cancelled step and the skipped remainde
|
|
|
40
40
|
assert.equal(chain.status, 'cancelled');
|
|
41
41
|
assert.equal(
|
|
42
42
|
renderSkillChain(chain),
|
|
43
|
-
['wiki-sync', '', '✓
|
|
43
|
+
['wiki-sync', '', '✓ Step 1/3', ' done', '× Step 2/3', ' cancelled', '– Step 3/3', ' skipped · chain_cancelled'].join('\n'),
|
|
44
44
|
);
|
|
45
45
|
});
|
|
46
46
|
|
|
@@ -9,7 +9,7 @@ import { formatSkillsForAgent, inspectSkills } from './skills.js';
|
|
|
9
9
|
test('matchSkillInvocation resolves only a real workspace skill', () => {
|
|
10
10
|
const root = mkdtempSync(join(tmpdir(), 'skill-invocation-'));
|
|
11
11
|
mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
|
|
12
|
-
writeFileSync(join(root, '.wiki', 'skills', 'deliver.md'), '---\nname: deliver\nparams:\n -
|
|
12
|
+
writeFileSync(join(root, '.wiki', 'skills', 'deliver.md'), '---\nname: deliver\nparams:\n - deliverable\n - polish\n---\nDeliver.');
|
|
13
13
|
const match = matchSkillInvocation({ workspacePath: root }, '/deliver "architecture" "improve security"');
|
|
14
14
|
assert.equal(match.skill.name, 'deliver');
|
|
15
15
|
assert.equal(match.rawArgs, '"architecture" "improve security"');
|
|
@@ -18,7 +18,7 @@ test('matchSkillInvocation resolves only a real workspace skill', () => {
|
|
|
18
18
|
|
|
19
19
|
test('parseSkillArguments preserves one free-form argument and parses quoted multi params', () => {
|
|
20
20
|
assert.deepEqual(parseSkillArguments({ params: ['files'] }, 'document A.md document B.md'), { files: 'document A.md document B.md' });
|
|
21
|
-
assert.deepEqual(parseSkillArguments({ params: ['
|
|
21
|
+
assert.deepEqual(parseSkillArguments({ params: ['deliverable', 'polish'] }, '"architecture-juno" "améliorer la sécurité réseau"'), { deliverable: 'architecture-juno', polish: 'améliorer la sécurité réseau' });
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
test('legacy placeholders remain supported and are reported', () => {
|
|
@@ -66,8 +66,8 @@ test('catalog renders declared parameters and marks a missing description explic
|
|
|
66
66
|
const root = mkdtempSync(join(tmpdir(), 'skill-catalog-'));
|
|
67
67
|
const dir = join(root, '.wiki', 'skills');
|
|
68
68
|
mkdirSync(dir, { recursive: true });
|
|
69
|
-
writeFileSync(join(dir, 'deliver.md'), '---\nname: deliver\nparams:\n -
|
|
69
|
+
writeFileSync(join(dir, 'deliver.md'), '---\nname: deliver\nparams:\n - deliverable\n - polish\n---\nDeliver.');
|
|
70
70
|
const inspection = inspectSkills({ workspacePath: root });
|
|
71
71
|
assert.deepEqual(inspection.warnings.map((item) => item.reason), ['missing_description']);
|
|
72
|
-
assert.match(formatSkillsForAgent({ workspacePath: root }), /\/deliver \[<
|
|
72
|
+
assert.match(formatSkillsForAgent({ workspacePath: root }), /\/deliver \[<deliverable> <polish>\]: workflow skill \[explicit name only\]/);
|
|
73
73
|
});
|
package/src/runtime/runner.js
CHANGED
|
@@ -14,6 +14,7 @@ import { assertValidatedFragment } from '../orchestrator/planValidator.js';
|
|
|
14
14
|
import { createResultAggregator } from '../orchestrator/resultAggregator.js';
|
|
15
15
|
import { describePlanConcurrency, drainActive, startReadyTasks } from '../orchestrator/scheduler.js';
|
|
16
16
|
import { emitRuntimeLog, pollActivitiesOnce } from './supervisor.js';
|
|
17
|
+
import { shortLogId } from '../core/runtimeLog.js';
|
|
17
18
|
|
|
18
19
|
// 0 by default: automatic replans turn evaluator/replanner TEXT into
|
|
19
20
|
// executable pseudo-tasks (no capability, no operation) that stall at 0%
|
|
@@ -713,7 +714,7 @@ export function skipImpossibleTasks(session, runId, { maxPasses = 50 } = {}) {
|
|
|
713
714
|
taskId: skippedId,
|
|
714
715
|
payload: { taskId: skippedId, status: 'skipped', reason: `dependency_failed:${because}` },
|
|
715
716
|
}));
|
|
716
|
-
emitRuntimeLog(session, `scheduler: skipping ${skippedId} (dependency failed: ${because})`);
|
|
717
|
+
emitRuntimeLog(session, `scheduler: skipping ${shortLogId(skippedId)} (dependency failed: ${because.split(', ').map((dep) => shortLogId(dep)).join(', ')})`);
|
|
717
718
|
}
|
|
718
719
|
total += changed;
|
|
719
720
|
if (changed === 0) break;
|
|
@@ -1723,7 +1723,7 @@ test('POST /turn keeps informational skill and build questions conversational',
|
|
|
1723
1723
|
test('POST /run accepts named skill arguments and deduplicates an explicit retry key', async (t) => {
|
|
1724
1724
|
const root = mkdtempSync(join(tmpdir(), 'runtime-named-skill-'));
|
|
1725
1725
|
mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
|
|
1726
|
-
writeFileSync(join(root, '.wiki', 'skills', 'deliver.md'), '---\nname: deliver\nparams:\n -
|
|
1726
|
+
writeFileSync(join(root, '.wiki', 'skills', 'deliver.md'), '---\nname: deliver\nparams:\n - deliverable\n - polish\n---\nDeliver the output.');
|
|
1727
1727
|
const session = { workspace: 'acme', workspacePath: root, controlQueue: [] };
|
|
1728
1728
|
const context = { workspace: 'acme', session, running: false, currentAbortController: null };
|
|
1729
1729
|
const persisted = new Map();
|
|
@@ -1746,7 +1746,7 @@ test('POST /run accepts named skill arguments and deduplicates an explicit retry
|
|
|
1746
1746
|
try {
|
|
1747
1747
|
const request = () => fetch(`http://127.0.0.1:${handle.port}/run?workspace=acme`, {
|
|
1748
1748
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1749
|
-
body: JSON.stringify({ input: '/deliver', skillName: 'deliver', skillArguments: {
|
|
1749
|
+
body: JSON.stringify({ input: '/deliver', skillName: 'deliver', skillArguments: { deliverable: 'Quarterly report' }, idempotencyKey: 'retry-1' }),
|
|
1750
1750
|
});
|
|
1751
1751
|
const first = await (await request()).json();
|
|
1752
1752
|
const second = await (await request()).json();
|
|
@@ -1756,7 +1756,7 @@ test('POST /run accepts named skill arguments and deduplicates an explicit retry
|
|
|
1756
1756
|
assert.equal(session.controlQueue.length, 1);
|
|
1757
1757
|
assert.equal('input' in first.items[0], false);
|
|
1758
1758
|
assert.equal('objectives' in first, false);
|
|
1759
|
-
assert.equal(session.controlQueue[0].input, '/deliver
|
|
1759
|
+
assert.equal(session.controlQueue[0].input, '/deliver deliverable="Quarterly report"');
|
|
1760
1760
|
} finally {
|
|
1761
1761
|
context.currentAbortController?.abort();
|
|
1762
1762
|
await handle.close();
|
|
@@ -2,24 +2,24 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
import { formatPublicSkillInvocation, runSkillChain, validateNamedSkillArguments } from './skillRun.js';
|
|
4
4
|
|
|
5
|
-
const skill = { name: 'deliver', params: ['
|
|
5
|
+
const skill = { name: 'deliver', params: ['deliverable', 'polish'], body: 'Deliver the requested output.' };
|
|
6
6
|
|
|
7
7
|
test('named skill arguments preserve spaces and fill omitted declarations with empty strings', () => {
|
|
8
|
-
const args = validateNamedSkillArguments(skill, {
|
|
8
|
+
const args = validateNamedSkillArguments(skill, { deliverable: 'Quarterly report' });
|
|
9
9
|
assert.equal(Object.getPrototypeOf(args), null);
|
|
10
|
-
assert.deepEqual({ ...args }, {
|
|
10
|
+
assert.deepEqual({ ...args }, { deliverable: 'Quarterly report', polish: '' });
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
test('named skill arguments reject undeclared, non-string, and oversized values', () => {
|
|
14
14
|
assert.throws(() => validateNamedSkillArguments(skill, { target: 'x' }), { code: 'skill_arguments_invalid' });
|
|
15
|
-
assert.throws(() => validateNamedSkillArguments(skill, {
|
|
16
|
-
assert.throws(() => validateNamedSkillArguments(skill, {
|
|
15
|
+
assert.throws(() => validateNamedSkillArguments(skill, { deliverable: 42 }), { code: 'skill_arguments_invalid' });
|
|
16
|
+
assert.throws(() => validateNamedSkillArguments(skill, { deliverable: 'x'.repeat(2_001) }), { code: 'skill_arguments_invalid' });
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
test('runSkillChain enqueues named arguments without exposing a skill body field', async () => {
|
|
20
20
|
const queued = [];
|
|
21
21
|
const result = await runSkillChain({ session: {} }, skill, {
|
|
22
|
-
args: {
|
|
22
|
+
args: { deliverable: 'Quarterly report' },
|
|
23
23
|
enqueueControlRequest(_context, input, metadata) {
|
|
24
24
|
const item = { id: `item-${queued.length}`, input, status: 'queued', ...metadata };
|
|
25
25
|
queued.push(item);
|
|
@@ -28,15 +28,15 @@ test('runSkillChain enqueues named arguments without exposing a skill body field
|
|
|
28
28
|
drainControlQueue() {},
|
|
29
29
|
});
|
|
30
30
|
assert.equal(result.objectives, 1);
|
|
31
|
-
assert.match(queued[0].input, /
|
|
32
|
-
assert.equal(queued[0].publicInput, '/deliver
|
|
31
|
+
assert.match(queued[0].input, /deliverable: Quarterly report/);
|
|
32
|
+
assert.equal(queued[0].publicInput, '/deliver deliverable="Quarterly report"');
|
|
33
33
|
assert.equal(queued[0].skillExecution, 'orchestrated');
|
|
34
34
|
assert.equal('body' in queued[0], false);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
test('public skill invocation contains arguments but never compiled objective prose', () => {
|
|
38
|
-
const rendered = formatPublicSkillInvocation('deliver', {
|
|
39
|
-
assert.equal(rendered, '/deliver
|
|
38
|
+
const rendered = formatPublicSkillInvocation('deliver', { deliverable: 'Quarterly report', polish: '' });
|
|
39
|
+
assert.equal(rendered, '/deliver deliverable="Quarterly report"');
|
|
40
40
|
assert.doesNotMatch(rendered, /Deliver the requested output/);
|
|
41
41
|
});
|
|
42
42
|
|
package/src/shell/LeftPane.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { useRenderer } from '@opentui/solid';
|
|
|
5
5
|
import { colorForRenderedLine, helpCommandParts, keyValueParts, renderPlainMarkdown } from './renderer';
|
|
6
6
|
import { httpLinkParts, wrapHttpLinks } from './externalLinks.js';
|
|
7
7
|
import { normalizeExternalUrl } from './openExternal.js';
|
|
8
|
+
import { ActivityPanel } from './RightPane';
|
|
8
9
|
|
|
9
10
|
const LEGACY_DONNA_ROLE = 'do' + 't';
|
|
10
11
|
|
|
@@ -806,6 +807,7 @@ export function LeftPane(props: {
|
|
|
806
807
|
busy: boolean;
|
|
807
808
|
chatMode: boolean;
|
|
808
809
|
chatFocused: boolean;
|
|
810
|
+
activities: any[];
|
|
809
811
|
setInput: (value: string) => void;
|
|
810
812
|
submit: (value?: string) => void;
|
|
811
813
|
conversationRows: number;
|
|
@@ -851,6 +853,15 @@ export function LeftPane(props: {
|
|
|
851
853
|
onOpenLink={props.onOpenLink}
|
|
852
854
|
/>
|
|
853
855
|
)}
|
|
856
|
+
{/*
|
|
857
|
+
Activity strip. The run status used to live only in the right pane,
|
|
858
|
+
one glance away from where the reader types. A compact 4-line strip
|
|
859
|
+
above the composer keeps the current job in view while composing; the
|
|
860
|
+
right pane keeps the full Plan/Queue/Logs detail.
|
|
861
|
+
*/}
|
|
862
|
+
<box flexShrink={0} height={4} flexDirection="column" overflow="hidden">
|
|
863
|
+
<ActivityPanel activities={props.activities} width={props.width - 2} />
|
|
864
|
+
</box>
|
|
854
865
|
<ChatInput
|
|
855
866
|
width={props.width}
|
|
856
867
|
prompt={props.prompt}
|
package/src/shell/RightPane.tsx
CHANGED
|
@@ -9,6 +9,7 @@ type QueueItem = {
|
|
|
9
9
|
workspace?: string | null;
|
|
10
10
|
status: string;
|
|
11
11
|
args?: Record<string, any>;
|
|
12
|
+
label?: string;
|
|
12
13
|
jobId?: string;
|
|
13
14
|
error?: string;
|
|
14
15
|
reason?: string;
|
|
@@ -170,9 +171,10 @@ function activityJobName(activity: any) {
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
function queueSummary(item: QueueItem) {
|
|
174
|
+
const label = item.label ? String(item.label).trim() : '';
|
|
173
175
|
const args = item.args ?? {};
|
|
174
176
|
const parts = [
|
|
175
|
-
args.type
|
|
177
|
+
label || args.type || 'production',
|
|
176
178
|
Array.isArray(args.steps) && args.steps.length ? args.steps.join('+') : null,
|
|
177
179
|
Array.isArray(args.templates) && args.templates.length ? `tpl:${args.templates.length}` : null,
|
|
178
180
|
Array.isArray(args.deliverables) && args.deliverables.length ? `del:${args.deliverables.length}` : null,
|
|
@@ -180,10 +182,24 @@ function queueSummary(item: QueueItem) {
|
|
|
180
182
|
return parts.join(' ');
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
// A queue id is a long UUID (optionally `control-`-prefixed). The panel shows a
|
|
186
|
+
// short, recognisable prefix instead of the full string, which pushed the
|
|
187
|
+
// status and summary off the line and made the queue unreadable.
|
|
188
|
+
function shortQueueId(id: string) {
|
|
189
|
+
const value = String(id ?? '').replace(/^control-/, '');
|
|
190
|
+
return value.length > 12 ? `${value.slice(0, 12)}…` : value;
|
|
191
|
+
}
|
|
192
|
+
|
|
183
193
|
export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: string; summary?: string | null; spinnerFrame?: string }) {
|
|
184
194
|
// Keep one column for the native vertical scrollbar when the plan is long.
|
|
185
195
|
const lineWidth = () => Math.max(8, props.width - 3);
|
|
186
196
|
const firstPending = () => props.plan.find((s) => s.status === 'pending')?.step ?? null;
|
|
197
|
+
// A running step carries a thick left border, which costs one column: the
|
|
198
|
+
// wrap width is therefore one less for it. The same function feeds both the
|
|
199
|
+
// row-count memo and the render, so the viewport height can never undercount
|
|
200
|
+
// a running step's wrapped lines and clip the last one.
|
|
201
|
+
const isRunningStep = (step: PlanStep) => String(step.status ?? '').toLowerCase() === 'running';
|
|
202
|
+
const stepTextWidth = (step: PlanStep) => lineWidth() - (isRunningStep(step) ? 1 : 0);
|
|
187
203
|
const icon = (rawStatus: string) => {
|
|
188
204
|
const status = String(rawStatus ?? '').toLowerCase();
|
|
189
205
|
if (DONE_STATUSES.includes(status)) return '[✓]';
|
|
@@ -195,7 +211,7 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
|
|
|
195
211
|
return status === 'running' ? `[${props.spinnerFrame ?? '…'}]` : '[ ]';
|
|
196
212
|
};
|
|
197
213
|
const visualRows = createMemo(() => props.plan.reduce((total, step) =>
|
|
198
|
-
total + wrapLine(`${icon(step.status)} ${step.step}. ${step.description}`,
|
|
214
|
+
total + wrapLine(`${icon(step.status)} ${step.step}. ${step.description}`, stepTextWidth(step)).slice(0, 2).length, 0));
|
|
199
215
|
const title = () => {
|
|
200
216
|
const label = props.jobName ? `Plan : ${props.jobName}` : 'Plan';
|
|
201
217
|
return visualRows() > PLAN_VIEWPORT_ROWS ? `${label} (${props.plan.length}) · scroll` : label;
|
|
@@ -225,12 +241,14 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
|
|
|
225
241
|
{(step) => {
|
|
226
242
|
// Wrap step descriptions over up to 2 lines instead of truncating —
|
|
227
243
|
// "Ingest des 39 documents raw/untrac…" hid the actual target.
|
|
228
|
-
const
|
|
244
|
+
const running = () => isRunningStep(step());
|
|
245
|
+
const textWidth = () => stepTextWidth(step());
|
|
246
|
+
const lines = () => wrapLine(`${icon(step().status)} ${step().step}. ${step().description}`, textWidth()).slice(0, 2);
|
|
229
247
|
return (
|
|
230
|
-
<box flexShrink={0} flexDirection="column">
|
|
231
|
-
<text width={
|
|
248
|
+
<box flexShrink={0} flexDirection="column" border={running() ? ['left'] : undefined} borderStyle="heavy" borderColor="#89B4FA">
|
|
249
|
+
<text width={textWidth()} fg={planStepColor(step(), firstPending())} content={lines()[0]} />
|
|
232
250
|
<Show when={lines()[1]}>
|
|
233
|
-
<text width={
|
|
251
|
+
<text width={textWidth()} fg={planStepColor(step(), firstPending())} content={` ${fit(lines()[1], Math.max(8, textWidth() - 4))}`} />
|
|
234
252
|
</Show>
|
|
235
253
|
</box>
|
|
236
254
|
);
|
|
@@ -438,12 +456,12 @@ export function QueuePanel(props: { items: QueueItem[]; info: QueueInfo; width:
|
|
|
438
456
|
<text
|
|
439
457
|
width={lineWidth()}
|
|
440
458
|
fg={queueColor(item()?.status)}
|
|
441
|
-
content={item() ? fit(`${item()!.
|
|
459
|
+
content={item() ? fit(`${item()!.status} · ${queueSummary(item()!)}`, lineWidth()) : ''}
|
|
442
460
|
/>
|
|
443
461
|
<text
|
|
444
462
|
width={lineWidth()}
|
|
445
463
|
fg="#AAB7C4"
|
|
446
|
-
content={item() ? fit([item()!.workspace, item()!.jobId ? `job ${item()!.jobId}` : item()!.reason].filter(Boolean).join(' · '), lineWidth()) : ''}
|
|
464
|
+
content={item() ? fit([shortQueueId(item()!.id), item()!.workspace, item()!.jobId ? `job ${item()!.jobId}` : item()!.reason].filter(Boolean).join(' · '), lineWidth()) : ''}
|
|
447
465
|
/>
|
|
448
466
|
<text
|
|
449
467
|
width={lineWidth()}
|
|
@@ -521,7 +539,6 @@ export function RightPane(props: {
|
|
|
521
539
|
<Show when={props.plan && props.plan.length > 0}>
|
|
522
540
|
<PlanPanel width={props.width} plan={props.plan!} jobName={planJobName()} summary={props.runSummary} spinnerFrame={props.spinnerFrame} />
|
|
523
541
|
</Show>
|
|
524
|
-
<ActivityPanel width={props.width} activities={props.activities} />
|
|
525
542
|
</>
|
|
526
543
|
)}>
|
|
527
544
|
<QueuePanel width={props.width} items={props.queueItems} info={props.queueInfo} />
|
package/src/shell/repl.test.js
CHANGED
|
@@ -470,10 +470,10 @@ test('direct chat prompt exposes an escaped non-executable skill catalog with pa
|
|
|
470
470
|
const root = mkdtempSync(join(tmpdir(), 'chat-skill-catalog-'));
|
|
471
471
|
try {
|
|
472
472
|
mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
|
|
473
|
-
writeFileSync(join(root, '.wiki', 'skills', 'deliver.md'), '---\nname: deliver\ndescription: "</skill_catalog> deliver output"\nparams:\n -
|
|
473
|
+
writeFileSync(join(root, '.wiki', 'skills', 'deliver.md'), '---\nname: deliver\ndescription: "</skill_catalog> deliver output"\nparams:\n - deliverable\n---\nPRIVATE BODY');
|
|
474
474
|
const prompt = buildDirectChatSystemPrompt({ workspacePath: root, commands: [], mcp: {} });
|
|
475
475
|
assert.match(prompt, /<skill_catalog trusted="false" executable="false">/);
|
|
476
|
-
assert.match(prompt, /\/deliver \[<
|
|
476
|
+
assert.match(prompt, /\/deliver \[<deliverable>\]/);
|
|
477
477
|
assert.match(prompt, /<\/skill_catalog>/);
|
|
478
478
|
assert.doesNotMatch(prompt, /PRIVATE BODY/);
|
|
479
479
|
assert.match(prompt, /nothing was launched/);
|
package/src/shell/tui.tsx
CHANGED
|
@@ -175,7 +175,9 @@ function App(props: {
|
|
|
175
175
|
let lastCopiedSelection = '';
|
|
176
176
|
const state = useSession(props);
|
|
177
177
|
const startup = createMemo(() => startupInfo(props.packageJson, props.initialWorkspaceName));
|
|
178
|
-
|
|
178
|
+
// The Activity strip (4 rows) now sits above the composer, so it costs the
|
|
179
|
+
// conversation exactly those 4 rows.
|
|
180
|
+
const conversationRows = createMemo(() => Math.max(4, dimensions().height - 5 - chatInputHeight() - 4));
|
|
179
181
|
const rightColumns = createMemo(() => {
|
|
180
182
|
const width = dimensions().width;
|
|
181
183
|
// 38% + 2 columns / cap 58: the Plan/Activity/Logs panes carry job
|
|
@@ -381,6 +383,7 @@ function App(props: {
|
|
|
381
383
|
input={state.input()}
|
|
382
384
|
busy={state.busy()}
|
|
383
385
|
chatMode={state.chatMode()}
|
|
386
|
+
activities={state.activities()}
|
|
384
387
|
// The composer must lose focus for EVERY modal, not just the file
|
|
385
388
|
// editor. While the setup wizard was open the input stayed focused
|
|
386
389
|
// underneath it, so answering a wizard question also typed into the
|