@pellux/goodvibes-tui 1.1.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +24 -27
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// fleet-transcript.ts
|
|
3
|
+
//
|
|
4
|
+
// Wave-3 (W3.1 Part C6) — renders the content of an attached FleetPanel
|
|
5
|
+
// session tab. Three sources, chosen by what the SDK can actually provide
|
|
6
|
+
// (see the W3.1 brief's "central reality check" — a full-fidelity live
|
|
7
|
+
// transcript needs a ConversationMessageSnapshot[] history source, which
|
|
8
|
+
// only exists for a RUNNING agent or a just-completed one still inside the
|
|
9
|
+
// SDK's bounded retention ring):
|
|
10
|
+
//
|
|
11
|
+
// - 'live'/'frozen' — a non-empty ConversationMessageSnapshot[] from
|
|
12
|
+
// AgentManager.getConversationSnapshot(agentId), rendered through the
|
|
13
|
+
// EXACT SAME conversation-rendering machinery the main session surface
|
|
14
|
+
// uses (MessageLineCache -> conversation-rendering.ts render functions),
|
|
15
|
+
// so a fleet tab's transcript is byte-for-byte the same rendering a
|
|
16
|
+
// user would see in their own session. 'live' while the agent is still
|
|
17
|
+
// running, 'frozen' once it has completed but the snapshot has not yet
|
|
18
|
+
// been evicted from the retention ring.
|
|
19
|
+
// - 'unavailable' — a terminal agent whose snapshot came back empty
|
|
20
|
+
// (evicted past the retention bound, or an agent kind — e.g. the WRFC
|
|
21
|
+
// owner — that never registered a live conversation source at all).
|
|
22
|
+
// FleetPanel degrades to the on-disk event ledger (renderFleetLedgerFallback)
|
|
23
|
+
// for this case; this module never fabricates transcript content.
|
|
24
|
+
//
|
|
25
|
+
// A 'wrfc-chain' tab has no single conversation of its own (it coordinates
|
|
26
|
+
// member agents, each of which has its own), so it gets a live member
|
|
27
|
+
// summary (renderFleetChainSummary) instead of a transcript.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
31
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
32
|
+
import { createEmptyLine, type Line } from '../types/grid.ts';
|
|
33
|
+
import type { MessageLineCache } from '../core/conversation-line-cache.ts';
|
|
34
|
+
import type { ConversationRenderContext } from '../core/conversation-rendering.ts';
|
|
35
|
+
import { renderConversationEventLine, renderConversationNotice } from '../renderer/conversation-surface.ts';
|
|
36
|
+
import { buildPanelLine, DEFAULT_PANEL_PALETTE, type PanelPalette } from './polish.ts';
|
|
37
|
+
import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
38
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
39
|
+
import {
|
|
40
|
+
fleetKindTag,
|
|
41
|
+
fleetStateGlyph,
|
|
42
|
+
fleetStateTone,
|
|
43
|
+
type FleetStateTone,
|
|
44
|
+
type FleetTreeRow,
|
|
45
|
+
} from './fleet-read-model.ts';
|
|
46
|
+
|
|
47
|
+
/** Duplicated from fleet-panel.ts's private helper (same tiny mapping) rather than
|
|
48
|
+
* cross-importing — fleet-panel.ts imports THIS module to render tab content, so
|
|
49
|
+
* importing back would form a cycle. See fleet-panel.ts's own toneColor for the
|
|
50
|
+
* canonical copy; keep both in sync if the tone set ever changes. */
|
|
51
|
+
function toneColor(tone: FleetStateTone, palette: PanelPalette): string {
|
|
52
|
+
switch (tone) {
|
|
53
|
+
case 'active': return palette.info ?? DEFAULT_PANEL_PALETTE.info;
|
|
54
|
+
case 'success': return palette.good ?? DEFAULT_PANEL_PALETTE.good;
|
|
55
|
+
case 'failure': return palette.bad ?? DEFAULT_PANEL_PALETTE.bad;
|
|
56
|
+
case 'warn': return palette.warn ?? DEFAULT_PANEL_PALETTE.warn;
|
|
57
|
+
case 'muted': return palette.dim;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Agent transcript (C6 live/frozen split)
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export type FleetTranscriptMode = 'live' | 'frozen' | 'unavailable';
|
|
66
|
+
|
|
67
|
+
export interface FleetTranscriptRender {
|
|
68
|
+
readonly mode: FleetTranscriptMode;
|
|
69
|
+
readonly lines: Line[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* W3.3 (design point 4) — a 'frozen' transcript is a static capture of a
|
|
74
|
+
* completed process, not something that will change on the next render. The
|
|
75
|
+
* distinction matters because this wave also introduced kill/interrupt
|
|
76
|
+
* display bugs (elapsed/usage briefly climbing after a terminal state
|
|
77
|
+
* shows) — a done-section tab that looked indistinguishable from a live one
|
|
78
|
+
* would misleadingly suggest the underlying agent is still doing something.
|
|
79
|
+
* Shown once at the top of a 'frozen' tab's content; 'live' tabs never show
|
|
80
|
+
* it, and 'unavailable' tabs get the ledger fallback's own notice instead
|
|
81
|
+
* (renderFleetLedgerFallback below) rather than this one.
|
|
82
|
+
*/
|
|
83
|
+
const FROZEN_TRANSCRIPT_NOTICE = 'Read-only — this agent finished; not a live view.';
|
|
84
|
+
|
|
85
|
+
function renderFrozenTranscriptNotice(width: number): Line[] {
|
|
86
|
+
const palette = DEFAULT_PANEL_PALETTE;
|
|
87
|
+
return renderConversationNotice(FROZEN_TRANSCRIPT_NOTICE, width, {
|
|
88
|
+
accent: palette.warn ?? DEFAULT_PANEL_PALETTE.warn,
|
|
89
|
+
text: palette.dim,
|
|
90
|
+
dim: true,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Render an agent tab's transcript from a `ConversationMessageSnapshot[]`
|
|
96
|
+
* already fetched by the caller (this module does no I/O of its own — it is
|
|
97
|
+
* a pure renderer, same convention as fleet-read-model.ts).
|
|
98
|
+
*
|
|
99
|
+
* `isTerminal` distinguishes 'live' (the agent is still running — the
|
|
100
|
+
* snapshot came from the SDK's live source, so it will change on the next
|
|
101
|
+
* render) from 'frozen' (the agent finished — the snapshot is a static final
|
|
102
|
+
* capture; caller is showing a completed conversation, not a live one).
|
|
103
|
+
* An empty snapshot on a terminal agent means the source was never
|
|
104
|
+
* registered or has been evicted — the caller falls back to
|
|
105
|
+
* `renderFleetLedgerFallback`; an empty snapshot on a non-terminal agent
|
|
106
|
+
* (e.g. the SDK hasn't sent a turn yet) legitimately means "no messages yet".
|
|
107
|
+
*/
|
|
108
|
+
export function renderFleetAgentTranscript(
|
|
109
|
+
snapshot: readonly ConversationMessageSnapshot[],
|
|
110
|
+
isTerminal: boolean,
|
|
111
|
+
lineCache: MessageLineCache,
|
|
112
|
+
width: number,
|
|
113
|
+
height: number,
|
|
114
|
+
configManager: ConfigManager | null,
|
|
115
|
+
): FleetTranscriptRender {
|
|
116
|
+
if (snapshot.length === 0) {
|
|
117
|
+
if (isTerminal) return { mode: 'unavailable', lines: [] };
|
|
118
|
+
return { mode: 'live', lines: [buildPanelLine(width, [[' (no messages yet)', DEFAULT_PANEL_PALETTE.dim]])] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const historyLines: Line[] = [];
|
|
122
|
+
const history = {
|
|
123
|
+
addLine: (line: Line): void => { historyLines.push(line); },
|
|
124
|
+
addLines: (lines: Line[]): void => { historyLines.push(...lines); },
|
|
125
|
+
getLineCount: (): number => historyLines.length,
|
|
126
|
+
};
|
|
127
|
+
// Scratch block/collapse/error registries — collapse-toggle and block-copy
|
|
128
|
+
// interactions are out of scope for this tab (a later item, per the brief's
|
|
129
|
+
// "transcript browse" follow-on); this render is a read-only tail view.
|
|
130
|
+
const context: ConversationRenderContext = {
|
|
131
|
+
history,
|
|
132
|
+
blockRegistry: [],
|
|
133
|
+
collapseState: new Map(),
|
|
134
|
+
errorLineRegistry: [],
|
|
135
|
+
messageKindRegistry: new Map(),
|
|
136
|
+
configManager,
|
|
137
|
+
splashOptions: {},
|
|
138
|
+
};
|
|
139
|
+
const messageLineRegistry: number[] = [];
|
|
140
|
+
lineCache.renderInto(context, snapshot as ConversationMessageSnapshot[], width, messageLineRegistry, 0, -1);
|
|
141
|
+
|
|
142
|
+
// Terminal agents reserve room for the frozen-transcript notice ABOVE the
|
|
143
|
+
// tail window so the "read-only" framing is always visible, never scrolled
|
|
144
|
+
// off by a long conversation's tail-window slice.
|
|
145
|
+
const notice = isTerminal ? renderFrozenTranscriptNotice(width) : [];
|
|
146
|
+
const budgetHeight = Math.max(0, height - notice.length);
|
|
147
|
+
// No `&& budgetHeight > 0` guard: when the notice alone already consumes
|
|
148
|
+
// the whole height budget, `historyLines.slice(historyLines.length - 0)`
|
|
149
|
+
// correctly yields an empty tail (nothing fits), rather than falling
|
|
150
|
+
// through to the full, un-sliced history — which the caller would then
|
|
151
|
+
// head-clip, silently swapping in the OLDEST lines instead of the
|
|
152
|
+
// most-recent tail this view promises.
|
|
153
|
+
const visible = historyLines.length > budgetHeight
|
|
154
|
+
? historyLines.slice(historyLines.length - budgetHeight)
|
|
155
|
+
: historyLines;
|
|
156
|
+
return { mode: isTerminal ? 'frozen' : 'live', lines: [...notice, ...visible] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Chain summary — 'wrfc-chain' tabs have no single conversation (C2/C6)
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Render a live one-line-per-member summary for an attached wrfc-chain tab.
|
|
165
|
+
*
|
|
166
|
+
* `chainDoneOrAbsent` disambiguates an empty member list (d3, W6.2): a completed
|
|
167
|
+
* chain prunes its wrapper node (Wave-3 reap), so zero members can mean the
|
|
168
|
+
* chain FINISHED, not that it has not started. When the chain node is absent
|
|
169
|
+
* (pruned) or terminal, say so honestly instead of the "(no member agents yet)"
|
|
170
|
+
* not-started wording.
|
|
171
|
+
*/
|
|
172
|
+
export function renderFleetChainSummary(
|
|
173
|
+
memberRows: readonly FleetTreeRow[],
|
|
174
|
+
width: number,
|
|
175
|
+
chainDoneOrAbsent: boolean,
|
|
176
|
+
): Line[] {
|
|
177
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
178
|
+
if (memberRows.length === 0) {
|
|
179
|
+
const message = chainDoneOrAbsent
|
|
180
|
+
? ' chain completed — members no longer tracked'
|
|
181
|
+
: ' (no member agents yet)';
|
|
182
|
+
return [buildPanelLine(width, [[message, C.dim]])];
|
|
183
|
+
}
|
|
184
|
+
return memberRows.map((row) => {
|
|
185
|
+
const node = row.node;
|
|
186
|
+
const color = toneColor(fleetStateTone(node.state), C);
|
|
187
|
+
return buildPanelLine(width, [
|
|
188
|
+
[' ', C.dim],
|
|
189
|
+
[fleetStateGlyph(node.state), color],
|
|
190
|
+
[` ${fleetKindTag(node.kind)} `, C.dim],
|
|
191
|
+
[node.label, C.value],
|
|
192
|
+
[` ${node.state}`, color],
|
|
193
|
+
[` ${formatElapsed(node.elapsedMs)}`, C.dim],
|
|
194
|
+
]);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Ledger fallback — degraded activity view for a terminal agent whose
|
|
200
|
+
// full-fidelity snapshot is unavailable (C6 "honest fallback")
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse an agent's `<agentId>.jsonl` event ledger. Tolerant of malformed
|
|
205
|
+
* lines (skipped, not thrown) — matches agent-detail-modal.ts's existing
|
|
206
|
+
* JSONL-reading convention for the same file shape.
|
|
207
|
+
*/
|
|
208
|
+
export function parseAgentLedger(raw: string): Record<string, unknown>[] {
|
|
209
|
+
const entries: Record<string, unknown>[] = [];
|
|
210
|
+
for (const line of raw.split('\n')) {
|
|
211
|
+
const trimmed = line.trim();
|
|
212
|
+
if (!trimmed) continue;
|
|
213
|
+
try {
|
|
214
|
+
const value: unknown = JSON.parse(trimmed);
|
|
215
|
+
if (value !== null && typeof value === 'object') entries.push(value as Record<string, unknown>);
|
|
216
|
+
} catch {
|
|
217
|
+
// Skip malformed lines rather than failing the whole fallback view.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return entries;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function formatLedgerDuration(ms: unknown): string {
|
|
224
|
+
return typeof ms === 'number' ? ` (${formatElapsed(ms)})` : '';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function renderLedgerEntry(width: number, entry: Record<string, unknown>, palette: PanelPalette): Line {
|
|
228
|
+
const type = typeof entry['type'] === 'string' ? entry['type'] : 'unknown';
|
|
229
|
+
switch (type) {
|
|
230
|
+
case 'meta': {
|
|
231
|
+
const model = typeof entry['model'] === 'string' ? entry['model'] : 'unknown model';
|
|
232
|
+
const provider = typeof entry['provider'] === 'string' ? entry['provider'] : 'unknown';
|
|
233
|
+
return renderConversationEventLine(width, { marker: '●', markerFg: palette.dim, label: 'session', labelFg: palette.dim }, [
|
|
234
|
+
{ text: `started — ${model} (${provider})`, fg: palette.value },
|
|
235
|
+
]);
|
|
236
|
+
}
|
|
237
|
+
case 'session_config': {
|
|
238
|
+
const task = typeof entry['task'] === 'string' ? entry['task'] : '';
|
|
239
|
+
return renderConversationEventLine(width, { marker: '●', markerFg: palette.dim, label: 'task', labelFg: palette.dim }, [
|
|
240
|
+
{ text: truncateDisplay(task, Math.max(0, width - 20)), fg: palette.value },
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
case 'llm_request': {
|
|
244
|
+
const turn = typeof entry['turn'] === 'number' ? entry['turn'] : '?';
|
|
245
|
+
return renderConversationEventLine(width, { marker: '◔', markerFg: palette.info, label: 'request', labelFg: palette.info }, [
|
|
246
|
+
{ text: `turn ${turn}`, fg: palette.dim },
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
case 'llm_response': {
|
|
250
|
+
const turn = typeof entry['turn'] === 'number' ? entry['turn'] : '?';
|
|
251
|
+
const toolCallCount = typeof entry['toolCallCount'] === 'number' ? entry['toolCallCount'] : 0;
|
|
252
|
+
return renderConversationEventLine(width, { marker: '◕', markerFg: palette.info, label: 'response', labelFg: palette.info }, [
|
|
253
|
+
{ text: `turn ${turn} · ${toolCallCount} tool call${toolCallCount === 1 ? '' : 's'}`, fg: palette.dim },
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
case 'tool_execution': {
|
|
257
|
+
const toolName = typeof entry['toolName'] === 'string' ? entry['toolName'] : 'tool';
|
|
258
|
+
const success = entry['success'] !== false;
|
|
259
|
+
const bad = palette.bad ?? DEFAULT_PANEL_PALETTE.bad;
|
|
260
|
+
// W3.3: the writer already truncates this to 500 chars (session.ts's
|
|
261
|
+
// resultPreview field) — passed through verbatim (collapsed to one
|
|
262
|
+
// line), never re-summarized or fabricated, then tail-truncated again
|
|
263
|
+
// to fit the row.
|
|
264
|
+
const rawPreview = typeof entry['resultPreview'] === 'string' ? entry['resultPreview'] : '';
|
|
265
|
+
const preview = rawPreview.replace(/\s+/g, ' ').trim();
|
|
266
|
+
const label = `${toolName}${success ? '' : ' (failed)'}`;
|
|
267
|
+
const text = preview ? `${label} — ${preview}` : label;
|
|
268
|
+
return renderConversationEventLine(width, {
|
|
269
|
+
marker: success ? '●' : '✗',
|
|
270
|
+
markerFg: success ? palette.info : bad,
|
|
271
|
+
label: 'tool',
|
|
272
|
+
labelFg: success ? palette.info : bad,
|
|
273
|
+
}, [
|
|
274
|
+
{ text: truncateDisplay(text, Math.max(0, width - 12)), fg: success ? palette.value : bad },
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
case 'session_end': {
|
|
278
|
+
const status = typeof entry['status'] === 'string' ? entry['status'] : 'ended';
|
|
279
|
+
const good = palette.good ?? DEFAULT_PANEL_PALETTE.good;
|
|
280
|
+
const bad = palette.bad ?? DEFAULT_PANEL_PALETTE.bad;
|
|
281
|
+
const warn = palette.warn ?? DEFAULT_PANEL_PALETTE.warn;
|
|
282
|
+
const tone = status === 'completed' ? good : status === 'failed' ? bad : warn;
|
|
283
|
+
return renderConversationEventLine(width, { marker: '■', markerFg: tone, label: 'session', labelFg: tone }, [
|
|
284
|
+
{ text: `${status}${formatLedgerDuration(entry['durationMs'])}`, fg: tone },
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
// W5.2 (wo803) — Wave-5 wo801 per-turn passive knowledge injection
|
|
288
|
+
// records, appended to this same JSONL ledger as
|
|
289
|
+
// `{type:'knowledge_injection', turn, ...record}` (orchestrator-runner.ts).
|
|
290
|
+
// Without this case these fell through to the generic 'event' default
|
|
291
|
+
// below (just the bare type name) — this renders the honest outcome
|
|
292
|
+
// (what was injected, or why nothing was) instead.
|
|
293
|
+
case 'knowledge_injection': {
|
|
294
|
+
const turn = typeof entry['turn'] === 'number' ? entry['turn'] : '?';
|
|
295
|
+
const injectedIds = Array.isArray(entry['injectedIds']) ? entry['injectedIds'] : [];
|
|
296
|
+
const tokenCost = typeof entry['tokenCost'] === 'number' ? entry['tokenCost'] : 0;
|
|
297
|
+
const backendTag = entry['embeddingBackend'] === 'fallback-lexical' ? ' [lexical fallback]' : '';
|
|
298
|
+
if (injectedIds.length === 0) {
|
|
299
|
+
const reason = typeof entry['reason'] === 'string'
|
|
300
|
+
? entry['reason']
|
|
301
|
+
: 'nothing cleared the relevance floor';
|
|
302
|
+
return renderConversationEventLine(width, { marker: '◇', markerFg: palette.dim, label: 'knowledge', labelFg: palette.dim }, [
|
|
303
|
+
{ text: `turn ${turn} · nothing injected — ${reason}${backendTag}`, fg: palette.dim },
|
|
304
|
+
]);
|
|
305
|
+
}
|
|
306
|
+
return renderConversationEventLine(width, { marker: '◇', markerFg: palette.info, label: 'knowledge', labelFg: palette.info }, [
|
|
307
|
+
{ text: `turn ${turn} · injected ${injectedIds.length} (~${tokenCost} tok)${backendTag}`, fg: palette.value },
|
|
308
|
+
]);
|
|
309
|
+
}
|
|
310
|
+
default:
|
|
311
|
+
return renderConversationEventLine(width, { marker: '·', markerFg: palette.dim, label: 'event', labelFg: palette.dim }, [
|
|
312
|
+
{ text: type, fg: palette.dim },
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Degraded activity view for a terminal agent whose full-fidelity
|
|
319
|
+
* conversation snapshot is unavailable (evicted, or never registered).
|
|
320
|
+
* Explicitly framed as an activity log, never presented as a transcript
|
|
321
|
+
* replay (the ledger is a truncated event record — see session.ts /
|
|
322
|
+
* orchestrator-runner.ts: tool args/results are sliced to 500 chars and
|
|
323
|
+
* assistant response TEXT is never written at all, only its length).
|
|
324
|
+
*
|
|
325
|
+
* Mirrors renderFleetAgentTranscript's notice-reservation above: the notice
|
|
326
|
+
* rows are reserved ABOVE the tail window and only the body is tail-sliced,
|
|
327
|
+
* so a long ledger's degraded-view cue is never the thing that scrolls off
|
|
328
|
+
* (previously this tail-sliced `[...notice, ...body]` as one array, which
|
|
329
|
+
* for any ledger longer than the tab height silently dropped the notice
|
|
330
|
+
* itself, leaving what looked like a raw, unexplained activity feed).
|
|
331
|
+
*/
|
|
332
|
+
export function renderFleetLedgerFallback(
|
|
333
|
+
entries: readonly Record<string, unknown>[],
|
|
334
|
+
width: number,
|
|
335
|
+
height: number,
|
|
336
|
+
): Line[] {
|
|
337
|
+
const palette = DEFAULT_PANEL_PALETTE;
|
|
338
|
+
const notice = renderConversationNotice(
|
|
339
|
+
'Read-only. Full transcript unavailable for this agent — showing its activity log instead.',
|
|
340
|
+
width,
|
|
341
|
+
{ accent: palette.warn, text: palette.dim, dim: true },
|
|
342
|
+
);
|
|
343
|
+
const body = entries.length === 0
|
|
344
|
+
? [buildPanelLine(width, [[' (no activity recorded)', palette.dim]])]
|
|
345
|
+
: entries.map((entry) => renderLedgerEntry(width, entry, palette));
|
|
346
|
+
const budgetHeight = Math.max(0, height - notice.length);
|
|
347
|
+
const visible = body.length > budgetHeight
|
|
348
|
+
? body.slice(body.length - budgetHeight)
|
|
349
|
+
: body;
|
|
350
|
+
return [...notice, ...visible];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** A blank tab-content placeholder (e.g. while a ledger load is in flight). */
|
|
354
|
+
export function renderFleetTranscriptLoading(width: number): Line[] {
|
|
355
|
+
return [buildPanelLine(width, [[' Loading…', DEFAULT_PANEL_PALETTE.dim]]), createEmptyLine(width)];
|
|
356
|
+
}
|
package/src/panels/index.ts
CHANGED
|
@@ -4,39 +4,15 @@ export { BasePanel } from './base-panel.ts';
|
|
|
4
4
|
export { PanelManager } from './panel-manager.ts';
|
|
5
5
|
export { TokenBudgetPanel } from './token-budget-panel.ts';
|
|
6
6
|
export { CostTrackerPanel } from './cost-tracker-panel.ts';
|
|
7
|
-
export { AgentInspectorPanel } from './agent-inspector-panel.ts';
|
|
8
|
-
export { WrfcPanel } from './wrfc-panel.ts';
|
|
9
|
-
export { ProviderHealthPanel } from './provider-health-panel.ts';
|
|
10
|
-
export { ProviderHealthTracker } from './provider-health-tracker.ts';
|
|
11
|
-
export type { ProviderHealth, ProviderStatus } from './provider-health-tracker.ts';
|
|
12
7
|
export { GitPanel } from './git-panel.ts';
|
|
13
|
-
export { SymbolOutlinePanel } from './symbol-outline-panel.ts';
|
|
14
|
-
export type { SymbolEntry, SymbolKind } from './symbol-outline-panel.ts';
|
|
15
|
-
export { SessionBrowserPanel } from './session-browser-panel.ts';
|
|
16
|
-
export { DocsPanel } from './docs-panel.ts';
|
|
17
|
-
export { ThinkingPanel } from './thinking-panel.ts';
|
|
18
|
-
export { ToolInspectorPanel } from './tool-inspector-panel.ts';
|
|
19
|
-
export { DebugPanel } from './debug-panel.ts';
|
|
20
|
-
export type { ApiCallEntry, ApiCallStatus } from './debug-panel.ts';
|
|
21
8
|
export { registerBuiltinPanels } from './builtin-panels.ts';
|
|
22
9
|
export type { BuiltinPanelDeps } from './builtin-panels.ts';
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
// W6.1 (the purge) — group B: plugins/skills panels are register-retired but
|
|
11
|
+
// their modules are retained for shared non-class exports (PluginManagerControls;
|
|
12
|
+
// discoverSkills + SkillRecord/SkillOrigin, consumed by command runtimes and the
|
|
13
|
+
// skills modal). The other group-A and group-B panel classes are deleted outright
|
|
14
|
+
// (their views migrated to config-modal surfaces in src/panels/modals/).
|
|
25
15
|
export { PluginsPanel } from './plugins-panel.ts';
|
|
26
16
|
export { SkillsPanel } from './skills-panel.ts';
|
|
27
|
-
export {
|
|
28
|
-
export {
|
|
29
|
-
export { CommunicationPanel } from './communication-panel.ts';
|
|
30
|
-
export { CockpitPanel } from './cockpit-panel.ts';
|
|
31
|
-
export { RemotePanel } from './remote-panel.ts';
|
|
32
|
-
export { ServicesPanel } from './services-panel.ts';
|
|
33
|
-
export { SubscriptionPanel } from './subscription-panel.ts';
|
|
34
|
-
export { HooksPanel } from './hooks-panel.ts';
|
|
35
|
-
export { SecurityPanel } from './security-panel.ts';
|
|
36
|
-
export { MarketplacePanel } from './marketplace-panel.ts';
|
|
37
|
-
export { SandboxPanel } from './sandbox-panel.ts';
|
|
38
|
-
export { ApprovalPanel } from './approval-panel.ts';
|
|
39
|
-
export { KnowledgeGraphPanel } from './knowledge-graph-panel.ts';
|
|
40
|
-
export { SystemMessagesPanel } from './system-messages-panel.ts';
|
|
41
|
-
export { PanelListPanel } from './panel-list-panel.ts';
|
|
42
|
-
export type { SystemMessageEntry, SystemMessagePriority } from './system-messages-panel.ts';
|
|
17
|
+
export { FleetPanel } from './fleet-panel.ts';
|
|
18
|
+
export type { FleetActionCallbacks } from './fleet-panel.ts';
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { MODAL_TONES } from './modal-theme.ts';
|
|
2
|
+
import { infoRow } from './modal-surface-helpers.ts';
|
|
3
|
+
import { listHookPointContracts } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
4
|
+
import type {
|
|
5
|
+
HookActivityTracker,
|
|
6
|
+
HookDefinition,
|
|
7
|
+
HookDispatcher,
|
|
8
|
+
HookWorkbench,
|
|
9
|
+
} from '@pellux/goodvibes-sdk/platform/hooks';
|
|
10
|
+
import type {
|
|
11
|
+
ConfigModalActionContext,
|
|
12
|
+
ConfigModalRow,
|
|
13
|
+
ConfigModalSurface,
|
|
14
|
+
ConfigModalTab,
|
|
15
|
+
ConfigModalView,
|
|
16
|
+
} from '../../input/config-modal-types.ts';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Hooks → config-modal surface (W6.1 group-B port). Two tabs: 'Hooks' (active
|
|
20
|
+
// registrations + contract posture) and 'Activity' (recent runtime activity +
|
|
21
|
+
// managed authoring + last simulation — the panel's expand-activity view, now a
|
|
22
|
+
// dedicated tab). All sources are in-memory (dispatcher/workbench/tracker hold
|
|
23
|
+
// live state), so buildView reads live and refresh() is a no-op. Toggle/remove/
|
|
24
|
+
// simulate route to their `/hooks` command path. Selection-blind port: the
|
|
25
|
+
// panel's selected-hook contract detail is folded into each hook row label.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export type HooksModalDispatcher = Pick<HookDispatcher, 'listHooks' | 'getChains'>;
|
|
29
|
+
export type HooksModalWorkbench = Pick<HookWorkbench, 'getHooksFilePath' | 'listManagedHooks' | 'listManagedChains' | 'listRecentActions' | 'getLastSimulation'>;
|
|
30
|
+
export type HooksModalActivityTracker = Pick<HookActivityTracker, 'listRecent'>;
|
|
31
|
+
|
|
32
|
+
export interface HooksModalDeps {
|
|
33
|
+
readonly hookDispatcher: HooksModalDispatcher;
|
|
34
|
+
readonly hookWorkbench: HooksModalWorkbench;
|
|
35
|
+
readonly hookActivityTracker: HooksModalActivityTracker;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type HookEntry = { pattern: string; hook: HookDefinition };
|
|
39
|
+
|
|
40
|
+
function rowId(entry: HookEntry, index: number): string {
|
|
41
|
+
return `${entry.pattern}#${entry.hook.name ?? index}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class HooksModalSurface implements ConfigModalSurface {
|
|
45
|
+
readonly name = 'hooks-modal';
|
|
46
|
+
readonly title = 'Hooks';
|
|
47
|
+
|
|
48
|
+
constructor(private readonly deps: HooksModalDeps) {}
|
|
49
|
+
|
|
50
|
+
readonly actions = [
|
|
51
|
+
{ key: 't', id: 'toggle', label: 'toggle', enabledFor: (_row: ConfigModalRow | null, tabId: string) => tabId === 'hooks' },
|
|
52
|
+
{ key: 'x', id: 'remove', label: 'remove', enabledFor: (_row: ConfigModalRow | null, tabId: string) => tabId === 'hooks' },
|
|
53
|
+
{ key: 's', id: 'simulate', label: 'simulate', enabledFor: (_row: ConfigModalRow | null, tabId: string) => tabId === 'hooks' },
|
|
54
|
+
{ key: 'r', id: 'refresh', label: 'refresh' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
private entryFrom(id: string): HookEntry | undefined {
|
|
58
|
+
return this.deps.hookDispatcher.listHooks().find((entry, index) => rowId(entry, index) === id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private hooksTab(): ConfigModalTab {
|
|
62
|
+
const hooks = this.deps.hookDispatcher.listHooks();
|
|
63
|
+
const chains = this.deps.hookDispatcher.getChains();
|
|
64
|
+
const contracts = listHookPointContracts();
|
|
65
|
+
const managedHooks = this.deps.hookWorkbench.listManagedHooks();
|
|
66
|
+
const hooksFilePath = this.deps.hookWorkbench.getHooksFilePath();
|
|
67
|
+
|
|
68
|
+
if (hooks.length === 0) {
|
|
69
|
+
const rows: ConfigModalRow[] = [
|
|
70
|
+
infoRow('empty:0', 'No hooks are currently registered.'),
|
|
71
|
+
infoRow('empty:1', `contracts ${contracts.length} chains ${chains.length} managed ${managedHooks.length}`, { dim: true }),
|
|
72
|
+
infoRow('empty:2', `hooks file: ${hooksFilePath}`, { dim: true }),
|
|
73
|
+
infoRow('empty:title', 'Next steps'),
|
|
74
|
+
infoRow('empty:hooks', '/hooks — review hook contracts and managed authoring actions', { dim: true }),
|
|
75
|
+
infoRow('empty:settings', '/settings — review hook/runtime behavior in the settings surface', { dim: true }),
|
|
76
|
+
];
|
|
77
|
+
return { id: 'hooks', label: 'Hooks', rows, emptyText: '' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const recentForStats = this.deps.hookActivityTracker.listRecent(3);
|
|
81
|
+
const denials = recentForStats.filter((r) => r.ok && r.decision === 'deny').length;
|
|
82
|
+
const errors = recentForStats.filter((r) => !r.ok).length;
|
|
83
|
+
const header = [`hooks ${hooks.length} chains ${chains.length} contracts ${contracts.length} denials ${denials} errors ${errors}`];
|
|
84
|
+
|
|
85
|
+
const rows: ConfigModalRow[] = hooks.map((entry, index) => {
|
|
86
|
+
const contract = contracts.find((c) => c.pattern === entry.pattern);
|
|
87
|
+
const contractPart = contract
|
|
88
|
+
? ` · ${contract.authority}/${contract.executionMode} deny=${contract.canDeny ? 'y' : 'n'} mut=${contract.canMutateInput ? 'y' : 'n'} inj=${contract.canInjectContext ? 'y' : 'n'}`
|
|
89
|
+
: ' · no exact contract';
|
|
90
|
+
return {
|
|
91
|
+
id: rowId(entry, index),
|
|
92
|
+
label: `${(entry.hook.name ?? '(unnamed)').padEnd(20)} ${entry.pattern.padEnd(28)} ${(entry.hook.enabled === false ? 'DISABLED' : 'ENABLED').padEnd(8)} ${entry.hook.type}${contractPart}`,
|
|
93
|
+
...(contract ? {} : { style: { fg: MODAL_TONES.warn } }),
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { id: 'hooks', label: 'Hooks', header, rows, hints: ['t toggle', 'x remove', 's simulate'] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private activityTab(): ConfigModalTab {
|
|
101
|
+
const rows: ConfigModalRow[] = [];
|
|
102
|
+
const recentActivity = this.deps.hookActivityTracker.listRecent(20);
|
|
103
|
+
if (recentActivity.length === 0) {
|
|
104
|
+
rows.push(infoRow('act:none', 'No hook activity recorded yet.', { dim: true }));
|
|
105
|
+
} else {
|
|
106
|
+
recentActivity.forEach((record, i) => {
|
|
107
|
+
const decisionText = record.ok ? (record.decision ?? 'ok') : 'error';
|
|
108
|
+
const color = !record.ok ? MODAL_TONES.bad : record.decision === 'deny' ? MODAL_TONES.warn : MODAL_TONES.good;
|
|
109
|
+
rows.push(infoRow(`act:${i}`, `${record.hookName.padEnd(18)} ${record.path.padEnd(26)} ${decisionText}`, { fg: color }));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
rows.push(infoRow('auth:title', 'Authoring'));
|
|
114
|
+
const recentAuthoring = this.deps.hookWorkbench.listRecentActions(3);
|
|
115
|
+
if (recentAuthoring.length === 0) {
|
|
116
|
+
rows.push(infoRow('auth:none', 'No managed hook authoring actions recorded yet.', { dim: true }));
|
|
117
|
+
} else {
|
|
118
|
+
recentAuthoring.forEach((action, i) => rows.push(infoRow(`auth:${i}`, `${action.kind.padEnd(14)} ${action.target}`, { dim: true })));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const lastSimulation = this.deps.hookWorkbench.getLastSimulation();
|
|
122
|
+
if (lastSimulation) {
|
|
123
|
+
rows.push(infoRow('sim:path', `last simulation: ${lastSimulation.eventPath}`));
|
|
124
|
+
rows.push(infoRow('sim:matches', `matches: hooks=${lastSimulation.matchedHooks.length} chains=${lastSimulation.matchedChains.length}`, { dim: true }));
|
|
125
|
+
}
|
|
126
|
+
return { id: 'activity', label: 'Activity', rows, emptyText: 'No hook activity recorded yet.' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
buildView(): ConfigModalView {
|
|
130
|
+
return { title: 'Hooks', tabs: [this.hooksTab(), this.activityTab()] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
134
|
+
if (id === 'refresh') { ctx.setStatus('Hooks are read live.'); ctx.requestRender(); return; }
|
|
135
|
+
const entry = ctx.row ? this.entryFrom(ctx.row.id) : undefined;
|
|
136
|
+
if (!entry) return;
|
|
137
|
+
const name = entry.hook.name;
|
|
138
|
+
switch (id) {
|
|
139
|
+
case 'toggle':
|
|
140
|
+
if (!name) { ctx.print('This hook has no managed name to toggle.'); return; }
|
|
141
|
+
void ctx.executeCommand?.('hooks', [entry.hook.enabled === false ? 'enable' : 'disable', name]);
|
|
142
|
+
ctx.setStatus(`Dispatched /hooks ${entry.hook.enabled === false ? 'enable' : 'disable'} ${name}.`);
|
|
143
|
+
break;
|
|
144
|
+
case 'remove':
|
|
145
|
+
if (!name) { ctx.print('This hook has no managed name to remove.'); return; }
|
|
146
|
+
void ctx.executeCommand?.('hooks', ['remove', name]);
|
|
147
|
+
ctx.setStatus(`Dispatched /hooks remove ${name}.`);
|
|
148
|
+
break;
|
|
149
|
+
case 'simulate':
|
|
150
|
+
void ctx.executeCommand?.('hooks', ['simulate', entry.pattern]);
|
|
151
|
+
ctx.setStatus(`Dispatched /hooks simulate ${entry.pattern}.`);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function createHooksModalSurface(deps: HooksModalDeps): ConfigModalSurface {
|
|
158
|
+
return new HooksModalSurface(deps);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Deterministic golden fixture: fixed dispatcher/workbench/activity-tracker
|
|
163
|
+
* stand-ins with two managed hooks (one enabled, one disabled) and one recorded
|
|
164
|
+
* activity entry. listHookPointContracts() reads the SDK's static contract table
|
|
165
|
+
* (no disk/wall-clock). All timestamps are fixed epoch numbers.
|
|
166
|
+
*/
|
|
167
|
+
export function hooksModalGoldenSurface(): ConfigModalSurface {
|
|
168
|
+
const hooks: HookEntry[] = [
|
|
169
|
+
{ pattern: 'Pre:tool:*', hook: { match: 'Pre:tool:*', type: 'command', command: 'echo pre', name: 'pre-tool-guard', enabled: true } },
|
|
170
|
+
{ pattern: 'Post:file:write', hook: { match: 'Post:file:write', type: 'command', command: 'echo post', name: 'post-write-log', enabled: false } },
|
|
171
|
+
];
|
|
172
|
+
const dispatcher: HooksModalDispatcher = { listHooks: () => hooks, getChains: () => [] };
|
|
173
|
+
const workbench: HooksModalWorkbench = {
|
|
174
|
+
getHooksFilePath: () => '/golden/hooks.json',
|
|
175
|
+
listManagedHooks: () => hooks,
|
|
176
|
+
listManagedChains: () => [],
|
|
177
|
+
listRecentActions: () => [{ kind: 'toggle', target: 'pre-tool-guard', timestamp: 0 }],
|
|
178
|
+
getLastSimulation: () => null,
|
|
179
|
+
};
|
|
180
|
+
const activityTracker: HooksModalActivityTracker = {
|
|
181
|
+
listRecent: () => [{
|
|
182
|
+
timestamp: 0, path: 'Pre:tool:*', specific: '*', pattern: 'Pre:tool:*',
|
|
183
|
+
hookName: 'pre-tool-guard', hookType: 'command', ok: true, decision: 'allow', durationMs: 1, async: false,
|
|
184
|
+
}],
|
|
185
|
+
};
|
|
186
|
+
return createHooksModalSurface({ hookDispatcher: dispatcher, hookWorkbench: workbench, hookActivityTracker: activityTracker });
|
|
187
|
+
}
|