@dotdrelle/wiki-manager 0.15.78 → 0.15.84
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/agent/graph.js +57 -3
- package/src/agent/graph.test.js +97 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/mcp.js +1 -1
- package/src/core/progressNotes.js +39 -0
- package/src/core/runtimeLog.js +5 -2
- package/src/core/runtimeLog.test.js +14 -0
- package/src/runtime/store.js +6 -1
- package/src/shell/RightPane.tsx +36 -1
- package/src/shell/repl.test.js +12 -0
- package/src/shell/tui.tsx +1 -0
- package/src/shell/useSession.ts +17 -1
- package/wiki-workspace +7 -5
package/package.json
CHANGED
package/src/agent/graph.js
CHANGED
|
@@ -23,10 +23,12 @@ import { RESERVED_SLASH_COMMANDS, explicitSkillReference, objectiveNamesSkill }
|
|
|
23
23
|
import { handleSlashCommand } from '../commands/slash.js';
|
|
24
24
|
import { extractActivity, formatActivitySummary, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
25
25
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
26
|
+
import { toolResultNote, toolStartNote } from '../core/progressNotes.js';
|
|
26
27
|
import { enqueueProductionJob, ensureJobQueue, formatQueue, productionLockBusy } from '../core/jobQueue.js';
|
|
27
28
|
import { loadWorkspaceProfile, updateWorkspaceProfilePreference } from '../core/profile.js';
|
|
28
29
|
import { artifactFromToolCall, currentArtifactFor, currentArtifactPromptLine, rememberArtifact } from '../core/currentArtifact.js';
|
|
29
30
|
import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
|
|
31
|
+
import { objectiveForResolution } from '../orchestrator/objectiveResolver.js';
|
|
30
32
|
import { fetchRuntimeState, postRuntimeCancel, postRuntimeControl, postRuntimeDelegate, postRuntimeKill, postRuntimeSkill } from '../runtime/client.js';
|
|
31
33
|
|
|
32
34
|
const MAX_TOOL_ITERATIONS = 80;
|
|
@@ -862,7 +864,20 @@ export async function handleRuntimeControlTool(session, tool, args = {}) {
|
|
|
862
864
|
if (tool === 'delegate') {
|
|
863
865
|
const objective = String(args.objective ?? '').trim();
|
|
864
866
|
if (!objective) return 'Delegation rejected: missing objective.';
|
|
865
|
-
|
|
867
|
+
/*
|
|
868
|
+
La garde anti-configuration est un gouvernail INTERACTIF : elle corrige
|
|
869
|
+
Donna quand une demande de chat (« configure le connecteur ») part en
|
|
870
|
+
délégation vers une capacité d'export au lieu du tool de setup. Elle
|
|
871
|
+
n'a pas sa place à l'intérieur d'un run de compétence compilée : là,
|
|
872
|
+
l'objectif est déjà le workflow autorisé (le compilateur a produit une
|
|
873
|
+
intention MÉTIER), et le résolveur du runtime est l'autorité de routage.
|
|
874
|
+
Le corps expédié de /wiki-sync en est la preuve — « Export every
|
|
875
|
+
configured Confluence source exactly as the connector is currently
|
|
876
|
+
configured » a été refusé comme « configuration du connecteur » alors
|
|
877
|
+
que l'export était la seule action demandée.
|
|
878
|
+
*/
|
|
879
|
+
const compiledSkillRun = Boolean(session?._currentRunIdentity) && normalizedSkillStack(session).length > 0;
|
|
880
|
+
const connectorConfig = compiledSkillRun ? null : connectorConfigurationTarget(session, objective);
|
|
866
881
|
if (connectorConfig?.setupTool) {
|
|
867
882
|
return `Delegation rejected: configuring or authenticating ${connectorConfig.serverName} is not an orchestrated export. Call the offered ${connectorConfig.serverName}__${connectorConfig.setupTool} tool directly and present its authorization instructions or URL to the user.`;
|
|
868
883
|
}
|
|
@@ -1048,7 +1063,6 @@ export function connectorConfigurationTarget(session, objective) {
|
|
|
1048
1063
|
.filter((message) => message?.role === 'user')
|
|
1049
1064
|
.map((message) => String(message?.content ?? ''))
|
|
1050
1065
|
.join(' ');
|
|
1051
|
-
const objectiveText = String(objective ?? '').trim().toLowerCase();
|
|
1052
1066
|
// The configuration keyword must describe the objective being delegated NOW,
|
|
1053
1067
|
// not something the user said earlier in the session. Matching it against
|
|
1054
1068
|
// recent conversation context made a stale "configurer le CME" message poison
|
|
@@ -1058,7 +1072,33 @@ export function connectorConfigurationTarget(session, objective) {
|
|
|
1058
1072
|
// "connector" as a substring, and production skills mention an optional
|
|
1059
1073
|
// messaging connector; the narrowed word-boundary form keeps that from
|
|
1060
1074
|
// misclassifying a business run as connector setup.)
|
|
1061
|
-
|
|
1075
|
+
//
|
|
1076
|
+
// Guardrail and notification sentences are stripped first (the same
|
|
1077
|
+
// objectiveForResolution pass the runtime resolver applies): "Never
|
|
1078
|
+
// reconfigure the existing credentials" inside /wiki-sync is an execution
|
|
1079
|
+
// constraint on the export, not the thing being asked.
|
|
1080
|
+
const objectiveText = objectiveForResolution(objective).toLowerCase();
|
|
1081
|
+
/*
|
|
1082
|
+
Deux familles de mots de configuration, volontairement séparées :
|
|
1083
|
+
|
|
1084
|
+
- « configured », « configuration » (et leurs formes françaises) sont le
|
|
1085
|
+
plus souvent du CONTEXTE passif — « export every configured source »,
|
|
1086
|
+
« as the connector is currently configured ». Ils ne comptent comme une
|
|
1087
|
+
intention de configuration que quand l'objectif ne porte aucun verbe
|
|
1088
|
+
métier : sans ce filtre, le corps expédié de /wiki-sync était refusé
|
|
1089
|
+
comme « configuration du connecteur » alors que son action est l'export.
|
|
1090
|
+
- le reste (configure/ing/er/ez… actifs, credentials, password, token,
|
|
1091
|
+
oauth, authenticate, connect, setup…) est une intention de configuration
|
|
1092
|
+
et compte toujours, même à côté d'un verbe métier (« configure the
|
|
1093
|
+
export » reste une demande de configuration).
|
|
1094
|
+
*/
|
|
1095
|
+
const weakConfigMention = /\b(?:mis|re)?configur(?:ed|é(?:e)?s?|ations?)\b/i;
|
|
1096
|
+
const businessMutationVerb = /\b(?:export|ingest|build|send|collect|fetch|import|retrieve|publish|polish|deliver|sync|notify|generate|convert|review|research)\b/i;
|
|
1097
|
+
const strongConfigIntent = /(?:credential|identifiant|mot de passe|password|\bpat\b|api[ _-]?token|\boauth\b|authent|sign[ -]?in|\bsetup\b|\bconnect(?:ed|ing|ion|ions)?\b|(?:^|[^a-zà-ÿ])(?:mis|re)?configur(?!ed\b|é(?:e)?s?\b|ations?\b))/i;
|
|
1098
|
+
if (!strongConfigIntent.test(objectiveText)) {
|
|
1099
|
+
if (!weakConfigMention.test(objectiveText)) return null;
|
|
1100
|
+
if (businessMutationVerb.test(objectiveText)) return null;
|
|
1101
|
+
}
|
|
1062
1102
|
const contextText = `${recentContext} ${objectiveText}`.trim().toLowerCase();
|
|
1063
1103
|
for (const [serverName, server] of Object.entries(session?.mcp ?? {})) {
|
|
1064
1104
|
if (server?.status !== 'connected' || !Array.isArray(server.tools) || server.tools.length === 0) continue;
|
|
@@ -1847,6 +1887,13 @@ export function createAgentGraph(options = {}) {
|
|
|
1847
1887
|
args: call.function.arguments ?? '{}',
|
|
1848
1888
|
summary: argsSummary || 'calling...',
|
|
1849
1889
|
});
|
|
1890
|
+
// The turn's own account of itself, in the conversation rather than in a
|
|
1891
|
+
// panel: an interactive turn publishes no plan and no activity, so this
|
|
1892
|
+
// is the only thing that tells the user what is running while it runs.
|
|
1893
|
+
emitAgentEvent(state.session, 'assistant_progress', 'tool', {
|
|
1894
|
+
callId: call.id,
|
|
1895
|
+
message: toolStartNote(toolName),
|
|
1896
|
+
});
|
|
1850
1897
|
// A plan represents work, never observation. Read-only inventory/status
|
|
1851
1898
|
// calls stay out of Plan even when Donna uses them to answer a question.
|
|
1852
1899
|
let minimalPlanActive = false;
|
|
@@ -2035,6 +2082,13 @@ export function createAgentGraph(options = {}) {
|
|
|
2035
2082
|
result: boundedResult,
|
|
2036
2083
|
summary: ok ? 'done' : 'failed',
|
|
2037
2084
|
});
|
|
2085
|
+
// A failure is the return the user most needs and the one that vanishes
|
|
2086
|
+
// most easily: the loop keeps going, and without this the turn ends with
|
|
2087
|
+
// an answer that never mentions the step that did not work.
|
|
2088
|
+
emitAgentEvent(state.session, 'assistant_progress', 'tool', {
|
|
2089
|
+
callId: call.id,
|
|
2090
|
+
message: toolResultNote(toolName, ok, ok ? '' : boundedResult),
|
|
2091
|
+
});
|
|
2038
2092
|
toolResultMessages.push({
|
|
2039
2093
|
role: 'tool',
|
|
2040
2094
|
tool_call_id: call.id,
|
package/src/agent/graph.test.js
CHANGED
|
@@ -83,6 +83,103 @@ test('a stale configure-connector message does not poison a later business deleg
|
|
|
83
83
|
assert.equal(target, null);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
test('the shipped wiki-sync export objective is not connector setup', () => {
|
|
87
|
+
// "configured"/"configuration" here are passive context on an EXPORT
|
|
88
|
+
// objective — the exact wording that used to be rejected as connector
|
|
89
|
+
// configuration and steered the run into cme_setup.
|
|
90
|
+
const target = connectorConfigurationTarget({
|
|
91
|
+
agentProjection: { conversation: [] },
|
|
92
|
+
mcp: {
|
|
93
|
+
cme: {
|
|
94
|
+
status: 'connected',
|
|
95
|
+
tools: [
|
|
96
|
+
{ name: 'cme_setup', description: 'Configure Confluence credentials.' },
|
|
97
|
+
{ name: 'cme_export_run', description: 'Run export.' },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}, 'Export every configured Confluence source exactly as the connector is currently configured, checking configuration and source availability first, waiting for the export to finish, and stopping without producing partial input if it fails or exports nothing. Do not build, export, polish or publish deliverables.');
|
|
102
|
+
assert.equal(target, null);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('a passive configuration mention without a business verb still routes to connector setup', () => {
|
|
106
|
+
const target = connectorConfigurationTarget({
|
|
107
|
+
agentProjection: { conversation: [] },
|
|
108
|
+
mcp: {
|
|
109
|
+
cme: {
|
|
110
|
+
status: 'connected',
|
|
111
|
+
tools: [
|
|
112
|
+
{ name: 'cme_setup', description: 'Configure Confluence credentials.' },
|
|
113
|
+
{ name: 'cme_export_run', description: 'Run export.' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}, 'The cme connector is not configured yet.');
|
|
118
|
+
assert.deepEqual(target, { serverName: 'cme', setupTool: 'cme_setup' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('an active configuration request still wins over a business verb in the same objective', () => {
|
|
122
|
+
const target = connectorConfigurationTarget({
|
|
123
|
+
agentProjection: { conversation: [] },
|
|
124
|
+
mcp: {
|
|
125
|
+
cme: {
|
|
126
|
+
status: 'connected',
|
|
127
|
+
tools: [
|
|
128
|
+
{ name: 'cme_setup', description: 'Configure Confluence credentials.' },
|
|
129
|
+
{ name: 'cme_export_run', description: 'Run export.' },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}, 'Configure the cme connector before the export.');
|
|
134
|
+
assert.deepEqual(target, { serverName: 'cme', setupTool: 'cme_setup' });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('a compiled skill objective delegates without the connector-configuration steer', async () => {
|
|
138
|
+
const delegated = [];
|
|
139
|
+
let calls = 0;
|
|
140
|
+
const session = sessionBase({
|
|
141
|
+
runtime: { url: 'http://runtime.test' },
|
|
142
|
+
_skillStack: ['wiki-sync'],
|
|
143
|
+
_currentRunIdentity: { runId: 'run-sync', turnId: 'run-sync:turn-1', workspace: 'docs', skillChain: { skillName: 'wiki-sync', execution: 'orchestrated' } },
|
|
144
|
+
_delegateWithinRun: async (objective) => { delegated.push(objective); return { runId: 'run-sync', summary: { tasks: 1, agent: 'cme' } }; },
|
|
145
|
+
mcp: {
|
|
146
|
+
cme: {
|
|
147
|
+
status: 'connected',
|
|
148
|
+
url: 'http://cme.test/mcp',
|
|
149
|
+
tools: [
|
|
150
|
+
{ name: 'cme_setup', description: 'Configure Confluence credentials.' },
|
|
151
|
+
{ name: 'cme_export_run', description: 'Run export.' },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
llm: {
|
|
156
|
+
async completeWithTools() {
|
|
157
|
+
calls += 1;
|
|
158
|
+
if (calls === 1) {
|
|
159
|
+
return {
|
|
160
|
+
content: null,
|
|
161
|
+
message: { role: 'assistant', content: null },
|
|
162
|
+
tool_calls: [{
|
|
163
|
+
id: 'delegate-sync',
|
|
164
|
+
type: 'function',
|
|
165
|
+
function: {
|
|
166
|
+
name: 'runtime__delegate',
|
|
167
|
+
arguments: JSON.stringify({ objective: 'Export every configured Confluence source exactly as the connector is currently configured, checking configuration and source availability first.' }),
|
|
168
|
+
},
|
|
169
|
+
}],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { content: 'Export delegated.', message: { role: 'assistant', content: 'Export delegated.' }, tool_calls: null };
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const result = await createAgentGraph().invoke({ input: 'run sync', session });
|
|
178
|
+
assert.deepEqual(delegated, ['Export every configured Confluence source exactly as the connector is currently configured, checking configuration and source availability first.']);
|
|
179
|
+
assert.equal(result.response, 'Export delegated.');
|
|
180
|
+
assert.doesNotMatch(result.response, /Delegation rejected/);
|
|
181
|
+
});
|
|
182
|
+
|
|
86
183
|
test('Donna cannot answer an explicit action with manual instructions instead of delegating', async () => {
|
|
87
184
|
const originalFetch = globalThis.fetch;
|
|
88
185
|
let delegated = false;
|
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.84';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Deterministic, English-only progress notes Donna publishes during a turn.
|
|
2
|
+
//
|
|
3
|
+
// Same reasoning as controlMessages.js, and the same trade-off: spending an LLM
|
|
4
|
+
// turn to say "I am now calling template_write" would reintroduce exactly the
|
|
5
|
+
// per-message cost the orchestration refactor removed, and would double the
|
|
6
|
+
// latency of the very turn the note exists to explain. So these are written
|
|
7
|
+
// here, deterministically, in English — the one language this lane can
|
|
8
|
+
// guarantee — and never localized by a hardcoded fr/en catalog. Donna's own
|
|
9
|
+
// localized prose stays what it has always been: the final answer.
|
|
10
|
+
//
|
|
11
|
+
// They are published as `assistant_progress`, which store.js keeps OUT of the
|
|
12
|
+
// persisted event log. That is load-bearing: every persisted event feeds the
|
|
13
|
+
// conversation projection, which seeds the next turn's LLM context — so a
|
|
14
|
+
// progress note that persisted would be re-read by the model on every later
|
|
15
|
+
// turn, growing the context with commentary about work already finished.
|
|
16
|
+
//
|
|
17
|
+
// Phrasing is generic on purpose. A per-tool verb catalog ("Reading a wiki
|
|
18
|
+
// page…", "Writing the template…") reads better for the tools it knows and
|
|
19
|
+
// drifts silently the day a tool is added, which is how the shipped help came
|
|
20
|
+
// to advertise six skills against eleven. The tool name is data; the sentence
|
|
21
|
+
// around it is prose.
|
|
22
|
+
|
|
23
|
+
export function toolStartNote(name) {
|
|
24
|
+
return `Using ${name || 'a tool'}…`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function toolResultNote(name, ok, detail) {
|
|
28
|
+
const tool = name || 'the tool';
|
|
29
|
+
if (ok === false) {
|
|
30
|
+
const reason = String(detail ?? '').replace(/\s+/g, ' ').trim();
|
|
31
|
+
return reason ? `${tool} failed: ${reason}` : `${tool} failed.`;
|
|
32
|
+
}
|
|
33
|
+
return `${tool} finished.`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function turnDoneNote(steps) {
|
|
37
|
+
if (!steps) return 'Done.';
|
|
38
|
+
return `Done — ${steps} step${steps > 1 ? 's' : ''}.`;
|
|
39
|
+
}
|
package/src/core/runtimeLog.js
CHANGED
|
@@ -181,8 +181,11 @@ export function isDispatchPlumbingLine(line) {
|
|
|
181
181
|
// "Agent: classified input as …", "Agent: streaming final answer…" — describe
|
|
182
182
|
// how the agent works, not what the business run does. They belong with the
|
|
183
183
|
// dispatch plumbing in the Agent status tab; the Runtime tab keeps the
|
|
184
|
-
// business flow (plan, task transitions, failures).
|
|
185
|
-
|
|
184
|
+
// business flow (plan, task transitions, failures). "Agents: …" (plural — the
|
|
185
|
+
// slash-command lifecycle lines from `agents up`/`agents down`, e.g. "Agents:
|
|
186
|
+
// reloading manager environment…") is the same category and must match too:
|
|
187
|
+
// the singular-only pattern let those lines fall through to the Runtime tab.
|
|
188
|
+
const AGENT_TRACE_LINE = /^(?:runtime\s+)?(?:\d{1,2}:\d{2}(?::\d{2})?\s*(?:·\s*)?)?Agents?:\s/;
|
|
186
189
|
|
|
187
190
|
export function isAgentTraceLine(line) {
|
|
188
191
|
return AGENT_TRACE_LINE.test(String(line ?? ''));
|
|
@@ -170,6 +170,20 @@ test('isAgentTraceLine recognises the "Agent:" traces for the Agent status tab',
|
|
|
170
170
|
}
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
test('isAgentTraceLine also recognises the plural "Agents:" lifecycle lines', () => {
|
|
174
|
+
// The `agents up`/`agents down` slash-command lines are written in the
|
|
175
|
+
// plural ("Agents: reloading manager environment…") and were falling
|
|
176
|
+
// through the singular-only pattern into the Runtime tab.
|
|
177
|
+
for (const line of [
|
|
178
|
+
'13:55:25 Agents: reloading manager environment and MCP endpoints…',
|
|
179
|
+
'13:55:26 Agents: stack started degraded — one optional agent failed; the base agents are up.',
|
|
180
|
+
'13:55:27 Agents: docker output — exit code 1',
|
|
181
|
+
'runtime 13:55:25 Agents: starting external agents…',
|
|
182
|
+
]) {
|
|
183
|
+
assert.equal(isAgentTraceLine(line), true, `expected an agent trace: ${line}`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
173
187
|
test('isAgentTraceLine leaves every other line for the Runtime tab', () => {
|
|
174
188
|
for (const line of [
|
|
175
189
|
'14:42:18 ▸ Polish proposition — started (knowledge.polish → agent-production)',
|
package/src/runtime/store.js
CHANGED
|
@@ -20,7 +20,12 @@ import { markPersistedAgentsStale } from '../orchestrator/agentRegistry.js';
|
|
|
20
20
|
|
|
21
21
|
export { defaultRuntimeStateDir };
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// `assistant_progress` joins runtime_log here for a reason of its own: every
|
|
24
|
+
// persisted event feeds the conversation projection, which seeds the next
|
|
25
|
+
// turn's LLM context. A persisted progress note would be re-read by the model
|
|
26
|
+
// on every later turn as if it were something the user said or Donna answered,
|
|
27
|
+
// growing the context with commentary about work already finished.
|
|
28
|
+
const NON_PERSISTED_EVENT_TYPES = new Set(['runtime_log', 'assistant_progress']);
|
|
24
29
|
export const RUNTIME_STORE_SCHEMA_VERSION = 1;
|
|
25
30
|
const RUNTIME_RETENTION_DAYS = 30;
|
|
26
31
|
const TERMINAL_RUN_STATUSES = ['done', 'error', 'cancelled', 'interrupted'];
|
package/src/shell/RightPane.tsx
CHANGED
|
@@ -545,6 +545,36 @@ function TabHeader(props: { active: 'plan' | 'queue'; queueCount: number; onTabC
|
|
|
545
545
|
);
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
+
/*
|
|
549
|
+
Reject = cancel the run waiting for the approval, same semantics as the
|
|
550
|
+
served UI's Reject button (which also confirms before cancelling). One
|
|
551
|
+
click arms the confirmation so an accidental hit cannot silently discard a
|
|
552
|
+
pending plan; the armed state expires on its own when the second click
|
|
553
|
+
never comes.
|
|
554
|
+
*/
|
|
555
|
+
function RejectRunButton(props: { onReject: () => void }) {
|
|
556
|
+
const [confirming, setConfirming] = createSignal(false);
|
|
557
|
+
let resetTimer: ReturnType<typeof setTimeout> | null = null;
|
|
558
|
+
function toggle() {
|
|
559
|
+
if (resetTimer) clearTimeout(resetTimer);
|
|
560
|
+
if (confirming()) {
|
|
561
|
+
setConfirming(false);
|
|
562
|
+
props.onReject();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
setConfirming(true);
|
|
566
|
+
resetTimer = setTimeout(() => setConfirming(false), 4000);
|
|
567
|
+
}
|
|
568
|
+
return (
|
|
569
|
+
<text
|
|
570
|
+
fg={confirming() ? '#0B1020' : '#F38BA8'}
|
|
571
|
+
bg={confirming() ? '#F38BA8' : undefined}
|
|
572
|
+
content={confirming() ? ' Confirm cancel ' : ' Reject '}
|
|
573
|
+
onMouseUp={toggle}
|
|
574
|
+
/>
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
548
578
|
export function RightPane(props: {
|
|
549
579
|
width: number;
|
|
550
580
|
activities: any[];
|
|
@@ -557,6 +587,7 @@ export function RightPane(props: {
|
|
|
557
587
|
logFilter?: string;
|
|
558
588
|
pendingApprovals: any[];
|
|
559
589
|
onApprove: () => void;
|
|
590
|
+
onReject: () => void;
|
|
560
591
|
onTabClick: (tab: 'plan' | 'queue') => void;
|
|
561
592
|
spinnerFrame?: string;
|
|
562
593
|
}) {
|
|
@@ -578,7 +609,11 @@ export function RightPane(props: {
|
|
|
578
609
|
<Show when={props.pendingApprovals.length > 0}>
|
|
579
610
|
<box height={2} flexDirection="column" border={['left']} borderStyle="heavy" borderColor="#FBBF24" paddingX={1}>
|
|
580
611
|
<text fg="#FBBF24" content={`${props.pendingApprovals.length} approval(s) required`} />
|
|
581
|
-
<
|
|
612
|
+
<box flexDirection="row">
|
|
613
|
+
<text fg="#0B1020" bg="#FBBF24" content=" Approve run " onMouseUp={props.onApprove} />
|
|
614
|
+
<text fg="#4B5563" content=" " />
|
|
615
|
+
<RejectRunButton onReject={props.onReject} />
|
|
616
|
+
</box>
|
|
582
617
|
</box>
|
|
583
618
|
</Show>
|
|
584
619
|
<Show when={props.activeTab === 'queue'} fallback={(
|
package/src/shell/repl.test.js
CHANGED
|
@@ -169,6 +169,18 @@ test('ShellUI shows the canonical run summary above the plan', async () => {
|
|
|
169
169
|
assert.match(tui, /runSummary=\{state\.runSummary\(\)\}/);
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
test('ShellUI approval banner offers a reject next to approve, armed before it cancels', async () => {
|
|
173
|
+
const pane = await readFile(new URL('./RightPane.tsx', import.meta.url), 'utf8');
|
|
174
|
+
const tui = await readFile(new URL('./tui.tsx', import.meta.url), 'utf8');
|
|
175
|
+
// The banner no longer reduces a pending approval to "approve or nothing":
|
|
176
|
+
// reject cancels the waiting run, like the served UI's Reject button.
|
|
177
|
+
assert.match(pane, /content=" Approve run " onMouseUp=\{props\.onApprove\}/);
|
|
178
|
+
assert.match(pane, /content=\{confirming\(\) \? ' Confirm cancel ' : ' Reject '\}/);
|
|
179
|
+
assert.match(pane, /props\.onReject\(\)/);
|
|
180
|
+
assert.match(tui, /onReject=\{\(\) => \{ void state\.submitInput\('\/cancel'\); \}\}/);
|
|
181
|
+
assert.match(pane, /setTimeout\(\(\) => setConfirming\(false\), 4000\)/);
|
|
182
|
+
});
|
|
183
|
+
|
|
172
184
|
test('Flow/Trace does not repeat the runtime source prefix on every line', async () => {
|
|
173
185
|
const source = await readFile(new URL('./RightPane.tsx', import.meta.url), 'utf8');
|
|
174
186
|
const entryRenderer = source.slice(
|
package/src/shell/tui.tsx
CHANGED
|
@@ -434,6 +434,7 @@ function App(props: {
|
|
|
434
434
|
logFilter={state.runtimeLogFilter()}
|
|
435
435
|
pendingApprovals={state.pendingApprovals()}
|
|
436
436
|
onApprove={() => { void state.submitInput('/approve'); }}
|
|
437
|
+
onReject={() => { void state.submitInput('/cancel'); }}
|
|
437
438
|
onTabClick={state.selectRightTab}
|
|
438
439
|
spinnerFrame={SPINNER_FRAMES[spinnerIndex()] ?? SPINNER_FRAMES[0]}
|
|
439
440
|
/>
|
package/src/shell/useSession.ts
CHANGED
|
@@ -87,6 +87,21 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
87
87
|
// split classifies lines structurally from that time prefix.
|
|
88
88
|
setLogs((items) => [...items, `${new Date().toLocaleTimeString('en-GB', { hour12: false })} ${line}`].slice(-200));
|
|
89
89
|
};
|
|
90
|
+
// An interactive turn runs on an ephemeral runtime session: its events are
|
|
91
|
+
// published over SSE but never enter the projection /state serves, so syncing
|
|
92
|
+
// state alone leaves every panel empty for the whole turn.
|
|
93
|
+
// The sentence is taken from the event, never composed here — progressNotes.js
|
|
94
|
+
// is the single source for this lane, exactly as controlMessages.js is for the
|
|
95
|
+
// control lane ("never hardcode a message in the shell or the server").
|
|
96
|
+
// The `Agent:` prefix is the one thing added, and it is load-bearing:
|
|
97
|
+
// isAgentTraceLine routes these to the Agent status tab rather than mixing
|
|
98
|
+
// them into the business flow.
|
|
99
|
+
const logRuntimeAgentEvent = (event: { type?: string; data?: any }) => {
|
|
100
|
+
if (event?.type !== 'agent_event') return;
|
|
101
|
+
if (event.data?.type !== 'assistant_progress') return;
|
|
102
|
+
const message = String(event.data?.payload?.message ?? '').trim();
|
|
103
|
+
if (message) addLog(`Agent: ${message}`);
|
|
104
|
+
};
|
|
90
105
|
const runtimeUnavailableReason = createMemo(() => {
|
|
91
106
|
if (props.runtime?.url) return null;
|
|
92
107
|
const reason = props.runtime?.error ?? props.runtime?.unavailableReason ?? props.runtime?.reason ?? null;
|
|
@@ -537,12 +552,13 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
537
552
|
if (!props.runtime?.url || runtimeStreamStopped) return;
|
|
538
553
|
runtimeStreamAbort = new AbortController();
|
|
539
554
|
try {
|
|
540
|
-
for await (const
|
|
555
|
+
for await (const event of streamRuntimeEvents({
|
|
541
556
|
url: props.runtime.url,
|
|
542
557
|
signal: runtimeStreamAbort.signal,
|
|
543
558
|
workspace: (session as any).workspace ?? null,
|
|
544
559
|
})) {
|
|
545
560
|
setRuntimeStatus('connected');
|
|
561
|
+
logRuntimeAgentEvent(event);
|
|
546
562
|
debouncedSyncRuntimeState();
|
|
547
563
|
}
|
|
548
564
|
} catch {
|
package/wiki-workspace
CHANGED
|
@@ -140,7 +140,6 @@ wiki CLI commands reachable through `wiki <workspace> run`:
|
|
|
140
140
|
lint Static checks: dead links, orphans, stale deliverables
|
|
141
141
|
add-skill <source> Install a workspace method (directory, .zip, HTTPS .zip)
|
|
142
142
|
config Show the effective .wikirc.yaml
|
|
143
|
-
group-concepts Regroup wiki concepts
|
|
144
143
|
doctor, ingest, build, export Also available directly: wiki <workspace> <command>
|
|
145
144
|
|
|
146
145
|
History is versioned for wiki/, templates/, build-context/, deliverables/,
|
|
@@ -1352,10 +1351,13 @@ reset_workspace() {
|
|
|
1352
1351
|
# `.wikirc*`, templates/ and build-context/ come through untouched.
|
|
1353
1352
|
run_wiki "$workspace" init
|
|
1354
1353
|
|
|
1355
|
-
# The scaffold ships a demo
|
|
1356
|
-
#
|
|
1357
|
-
#
|
|
1358
|
-
|
|
1354
|
+
# The scaffold ships a handful of demo-*.md sources in raw/untracked/ (a
|
|
1355
|
+
# project brief plus a few notes chosen to classify into different concepts
|
|
1356
|
+
# with overlapping subjects/tags, so a first ingest already produces a
|
|
1357
|
+
# non-trivial graph). Reappearing here they would not read as samples but as
|
|
1358
|
+
# pending sources, and the next ingest would file them into the wiki as if
|
|
1359
|
+
# the user had provided them.
|
|
1360
|
+
rm -f -- "$ws_path"/raw/untracked/demo-*.md
|
|
1359
1361
|
|
|
1360
1362
|
printf 'Reset done: %s\n' "$workspace"
|
|
1361
1363
|
printf 'Kept: %s\n' "${kept[*]:-(nothing)}"
|