@pellux/goodvibes-tui 1.16.1 → 1.18.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 +138 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5490 -1806
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/management-commands.ts +82 -15
- package/src/cli/management.ts +0 -2
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +18 -9
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/memory-provenance.ts +67 -0
- package/src/core/pairing-devices.ts +49 -0
- package/src/core/pairing-handoff.ts +101 -0
- package/src/core/pairing-offers.ts +83 -0
- package/src/core/pairing-origin.ts +78 -0
- package/src/core/pairing-tailscale-gateway.ts +56 -0
- package/src/core/power-chip-source.ts +91 -0
- package/src/core/power-status.ts +95 -0
- package/src/core/stable-host.ts +171 -0
- package/src/core/stream-event-wiring.ts +29 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +40 -1
- package/src/core/turn-event-wiring.ts +14 -8
- package/src/daemon/cli.ts +64 -58
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +55 -9
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +109 -0
- package/src/input/commands/git-runtime.ts +28 -4
- package/src/input/commands/graph-runtime.ts +45 -0
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +46 -27
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -2
- package/src/input/config-modal-types.ts +14 -3
- package/src/input/config-modal.ts +75 -13
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +29 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +21 -3
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +52 -57
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +27 -17
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +90 -90
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +83 -11
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +16 -4
- package/src/panels/builtin-modals.ts +59 -42
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +39 -6
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +163 -11
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +73 -57
- package/src/panels/fleet-read-model.ts +84 -14
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +41 -5
- package/src/panels/fleet-tabs.ts +15 -16
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +153 -9
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +7 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +23 -5
- package/src/renderer/settings-modal.ts +129 -39
- package/src/renderer/shell-surface.ts +6 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +13 -7
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +38 -1
- package/src/runtime/bootstrap-command-context.ts +7 -2
- package/src/runtime/bootstrap-command-parts.ts +9 -5
- package/src/runtime/bootstrap-core.ts +41 -41
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +13 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +90 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +6 -21
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +113 -113
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +6 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +108 -132
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* broker-approval-card — render a broker-originated approval ask as a real
|
|
3
|
+
* TUI approval card.
|
|
4
|
+
*
|
|
5
|
+
* The approval broker publishes every ask (local runtime asks, plus
|
|
6
|
+
* broker-originated ones like the CI fix-session offer). Local asks carry a
|
|
7
|
+
* `localPrompt` that opens their own card; broker-originated asks do not, so
|
|
8
|
+
* before this they were invisible in the TUI — the ask sat pending with no
|
|
9
|
+
* surface. This helper opens a card straight from broker state for those.
|
|
10
|
+
*
|
|
11
|
+
* A local ask's own prompt is opened by the broker immediately AFTER it
|
|
12
|
+
* publishes (see ApprovalBroker.requestApproval), so the open here is deferred
|
|
13
|
+
* one microtask and re-checks: if a card (the local one, or another broker
|
|
14
|
+
* card) is already up, it does nothing — only a genuinely unhandled ask surfaces.
|
|
15
|
+
* The card's resolve answers the broker directly via resolveApproval, so a TUI
|
|
16
|
+
* decision on a broker-originated ask reaches every waiter on that record.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { buildPendingPermissionExtras } from './hunk-selection.ts';
|
|
20
|
+
import type { ApprovalRequesterLookup } from './hunk-selection.ts';
|
|
21
|
+
import type { PendingPermissionState } from '../shell/blocking-input.ts';
|
|
22
|
+
import type { PermissionPromptRequest, RememberTier } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
23
|
+
|
|
24
|
+
/** The broker seam this helper answers through — a subset of ApprovalBroker. */
|
|
25
|
+
export interface BrokerApprovalCardBroker extends ApprovalRequesterLookup {
|
|
26
|
+
getApproval(approvalId: string): { readonly status: string; readonly request: PermissionPromptRequest } | null;
|
|
27
|
+
resolveApproval(
|
|
28
|
+
approvalId: string,
|
|
29
|
+
input: {
|
|
30
|
+
readonly approved: boolean;
|
|
31
|
+
readonly remember?: boolean;
|
|
32
|
+
readonly rememberTier?: RememberTier;
|
|
33
|
+
readonly reason?: string;
|
|
34
|
+
readonly modifiedArgs?: Record<string, unknown>;
|
|
35
|
+
readonly actor: string;
|
|
36
|
+
readonly actorSurface?: string;
|
|
37
|
+
},
|
|
38
|
+
): Promise<unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface BrokerApprovalChangeParams {
|
|
42
|
+
readonly approval: {
|
|
43
|
+
readonly id: string;
|
|
44
|
+
readonly callId: string;
|
|
45
|
+
readonly status: string;
|
|
46
|
+
readonly request: PermissionPromptRequest;
|
|
47
|
+
/** The REAL attachable session id once an ACCEPTED ask (e.g. a ci:fix-session offer) has spawned its session. */
|
|
48
|
+
readonly fixSessionId?: string | undefined;
|
|
49
|
+
/** The honest failure when an accepted ask's spawn did NOT produce an attachable session — mutually exclusive with fixSessionId. */
|
|
50
|
+
readonly fixSessionError?: string | undefined;
|
|
51
|
+
};
|
|
52
|
+
readonly getPending: () => PendingPermissionState | null;
|
|
53
|
+
readonly setPending: (pending: PendingPermissionState | null) => void;
|
|
54
|
+
readonly broker: BrokerApprovalCardBroker;
|
|
55
|
+
readonly render: () => void;
|
|
56
|
+
/**
|
|
57
|
+
* Fired when a record carries the id of a session an accepted ask spawned
|
|
58
|
+
* (the CI fix-session). The surface that accepted — attached now — turns this
|
|
59
|
+
* into a jump/attach affordance. Called each time the field is seen; the
|
|
60
|
+
* caller de-duplicates by id.
|
|
61
|
+
*/
|
|
62
|
+
readonly onFixSessionStarted?: (fixSessionId: string) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Fired when a record carries the honest failure from an accepted ask whose
|
|
65
|
+
* spawn produced no attachable session. The surface renders it as an error
|
|
66
|
+
* line with NO action to jump to (there is no session). Called each time the
|
|
67
|
+
* field is seen; the caller de-duplicates.
|
|
68
|
+
*/
|
|
69
|
+
readonly onFixSessionError?: (fixSessionError: string) => void;
|
|
70
|
+
/** Defers the open (default queueMicrotask); injectable so tests run it synchronously. */
|
|
71
|
+
readonly defer?: (callback: () => void) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isActiveStatus = (status: string): boolean => status === 'pending' || status === 'claimed';
|
|
75
|
+
|
|
76
|
+
export interface FixSessionAffordanceDeps {
|
|
77
|
+
/** Announce the ready fix session as one actionable line — never a retype instruction. */
|
|
78
|
+
readonly notify: (message: string) => void;
|
|
79
|
+
/** Arm the one-key jump with the spawned session id; the surface consumes it on the jump key. */
|
|
80
|
+
readonly arm: (fixSessionId: string) => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build the one-key jump/attach affordance for a spawned fix-session: the first
|
|
85
|
+
* time a given session id is seen it ARMS the jump (the surface attaches on the
|
|
86
|
+
* jump key — the machine does the resume, the user never retypes an id) and
|
|
87
|
+
* surfaces a one-line actionable notice. Repeats are no-ops (de-duplicated by
|
|
88
|
+
* id), so the live record update and the listApprovals refresh path can both
|
|
89
|
+
* feed it safely.
|
|
90
|
+
*/
|
|
91
|
+
export function buildFixSessionAffordance(deps: FixSessionAffordanceDeps): (fixSessionId: string) => void {
|
|
92
|
+
const seen = new Set<string>();
|
|
93
|
+
return (fixSessionId) => {
|
|
94
|
+
if (seen.has(fixSessionId)) return;
|
|
95
|
+
seen.add(fixSessionId);
|
|
96
|
+
deps.arm(fixSessionId);
|
|
97
|
+
deps.notify('[CI] Fix session ready — press j to jump to it.');
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the honest failure notice for an accepted fix-session offer whose spawn
|
|
103
|
+
* produced no attachable session: one error line, NO jump action (there is no
|
|
104
|
+
* session to attach to). De-duplicated by the error text so the live record
|
|
105
|
+
* update and the listApprovals refresh path can both feed it safely.
|
|
106
|
+
*/
|
|
107
|
+
export function buildFixSessionErrorNotice(notify: (message: string) => void): (fixSessionError: string) => void {
|
|
108
|
+
const seen = new Set<string>();
|
|
109
|
+
return (fixSessionError) => {
|
|
110
|
+
if (seen.has(fixSessionError)) return;
|
|
111
|
+
seen.add(fixSessionError);
|
|
112
|
+
notify(`[CI] Fix session could not start: ${fixSessionError}`);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface FixSessionAttachKeyDeps {
|
|
117
|
+
/** The armed fix-session id (the surface clears the arm before calling this). */
|
|
118
|
+
readonly armedFixSessionId: string;
|
|
119
|
+
/** Attach/jump to the fix session — runs the resume so the user never retypes an id. */
|
|
120
|
+
readonly attach: (fixSessionId: string) => void;
|
|
121
|
+
readonly render: () => void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Consume the keypress that follows an armed fix-session notice: the jump key
|
|
126
|
+
* ('j') attaches to the armed session; any other key just falls through (the
|
|
127
|
+
* caller has already cleared the arm, so the affordance is one-shot). Returns
|
|
128
|
+
* true only when it performed the jump.
|
|
129
|
+
*/
|
|
130
|
+
export function handleFixSessionAttachKey(data: string, deps: FixSessionAttachKeyDeps): boolean {
|
|
131
|
+
if (data === 'j') {
|
|
132
|
+
deps.attach(deps.armedFixSessionId);
|
|
133
|
+
deps.render();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The refresh path: sweep current approval records for spawned fix-sessions and
|
|
141
|
+
* feed each to the (de-duplicating) affordance. Catches a session stamped before
|
|
142
|
+
* the live subscription attached, or a record update the subscription missed.
|
|
143
|
+
*/
|
|
144
|
+
export function refreshFixSessionsFromApprovals(
|
|
145
|
+
listApprovals: () => readonly { readonly fixSessionId?: string | undefined; readonly fixSessionError?: string | undefined }[],
|
|
146
|
+
onFixSessionStarted: (fixSessionId: string) => void,
|
|
147
|
+
onFixSessionError?: (fixSessionError: string) => void,
|
|
148
|
+
): void {
|
|
149
|
+
for (const record of listApprovals()) {
|
|
150
|
+
if (record.fixSessionId) onFixSessionStarted(record.fixSessionId);
|
|
151
|
+
else if (record.fixSessionError && onFixSessionError) onFixSessionError(record.fixSessionError);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* React to one broker approval change: clear the active card when ITS approval
|
|
157
|
+
* resolves, or open a card for a newly-pending broker-originated ask that no
|
|
158
|
+
* local prompt is handling.
|
|
159
|
+
*/
|
|
160
|
+
export function handleBrokerApprovalChange(params: BrokerApprovalChangeParams): void {
|
|
161
|
+
const { approval, getPending, setPending, broker, render } = params;
|
|
162
|
+
const defer = params.defer ?? queueMicrotask;
|
|
163
|
+
const active = isActiveStatus(approval.status);
|
|
164
|
+
|
|
165
|
+
// An accepted ask that spawned a session (the CI fix-session) publishes its
|
|
166
|
+
// REAL session id as a record update — the live in-process handle to jump to
|
|
167
|
+
// it — or, when the spawn failed, an honest error instead of a dead id
|
|
168
|
+
// (mutually exclusive). Both arrive after the card has resolved and cleared,
|
|
169
|
+
// so surface them independently of the card lifecycle below.
|
|
170
|
+
if (approval.fixSessionId) params.onFixSessionStarted?.(approval.fixSessionId);
|
|
171
|
+
else if (approval.fixSessionError) params.onFixSessionError?.(approval.fixSessionError);
|
|
172
|
+
|
|
173
|
+
const pending = getPending();
|
|
174
|
+
if (pending && pending.callId === approval.callId) {
|
|
175
|
+
// This is the card already on screen — clear it once its approval resolves.
|
|
176
|
+
if (!active) { setPending(null); render(); }
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!active) return;
|
|
180
|
+
|
|
181
|
+
defer(() => {
|
|
182
|
+
if (getPending()) return; // a local card (or another broker card) is already up
|
|
183
|
+
const current = broker.getApproval(approval.id);
|
|
184
|
+
if (!current || !isActiveStatus(current.status)) return;
|
|
185
|
+
setPending({
|
|
186
|
+
...current.request,
|
|
187
|
+
...buildPendingPermissionExtras(current.request, (decision) => {
|
|
188
|
+
void broker.resolveApproval(approval.id, {
|
|
189
|
+
approved: decision.approved,
|
|
190
|
+
remember: decision.remember ?? false,
|
|
191
|
+
...(decision.rememberTier ? { rememberTier: decision.rememberTier } : {}),
|
|
192
|
+
...(decision.reason ? { reason: decision.reason } : {}),
|
|
193
|
+
...(decision.modifiedArgs ? { modifiedArgs: decision.modifiedArgs } : {}),
|
|
194
|
+
actor: 'tui',
|
|
195
|
+
actorSurface: 'tui',
|
|
196
|
+
}).catch(() => {});
|
|
197
|
+
}, broker),
|
|
198
|
+
} as PendingPermissionState);
|
|
199
|
+
render();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { PermissionAttribution, PermissionPromptRequest } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
1
|
+
import type { PermissionAttribution, PermissionPromptRequest, RememberTier } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Per-hunk accept/reject at the approval gate (
|
|
4
|
+
* Per-hunk accept/reject at the approval gate (v1).
|
|
5
5
|
*
|
|
6
6
|
* Scoped to the `edit` tool only: EditInput.edits is already an array of
|
|
7
7
|
* independent, atomic find/replace units (each a natural "hunk"), so no
|
|
@@ -48,6 +48,16 @@ export interface PermissionPromptDecisionWithModifiedArgs {
|
|
|
48
48
|
approved: boolean;
|
|
49
49
|
remember?: boolean;
|
|
50
50
|
modifiedArgs?: Record<string, unknown>;
|
|
51
|
+
/** Generalizing remember tier: 'session' | 'exact' | 'command-class' | 'path' | 'tool'. */
|
|
52
|
+
rememberTier?: RememberTier;
|
|
53
|
+
/** Free-text from the user; most useful on deny — rides the structured "user declined" tool result. */
|
|
54
|
+
reason?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Extra decision fields a resolve call can carry beyond approve/remember/modifiedArgs. */
|
|
58
|
+
export interface PermissionResolveExtras {
|
|
59
|
+
readonly rememberTier?: RememberTier;
|
|
60
|
+
readonly reason?: string;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
export interface HunkKeyResult {
|
|
@@ -247,14 +257,28 @@ export function buildPendingPermissionExtras(
|
|
|
247
257
|
now: number = Date.now(),
|
|
248
258
|
): {
|
|
249
259
|
hunkState: HunkSelectionState | undefined;
|
|
250
|
-
resolve: (approved: boolean, remember?: boolean, modifiedArgs?: Record<string, unknown
|
|
260
|
+
resolve: (approved: boolean, remember?: boolean, modifiedArgs?: Record<string, unknown>, extras?: PermissionResolveExtras) => void;
|
|
251
261
|
openedAt: number;
|
|
252
262
|
requestedBy: string | undefined;
|
|
263
|
+
replyMode: 'exec-answer' | undefined;
|
|
264
|
+
replyBuffer: string | undefined;
|
|
253
265
|
} {
|
|
266
|
+
// An exec-prompt ask (a running command waiting on stdin) opens straight in
|
|
267
|
+
// answer mode: every printable key types the answer — a run whose prompt
|
|
268
|
+
// needs a literal 'y' must never have it eaten as a card command key.
|
|
269
|
+
const isExecPrompt = request.attribution?.kind === 'exec-prompt';
|
|
254
270
|
return {
|
|
255
271
|
hunkState: isHunkSelectable(request) ? buildHunkSelectionState(request) : undefined,
|
|
256
|
-
resolve: (approved, remember = false, modifiedArgs) => resolvePromise({
|
|
272
|
+
resolve: (approved, remember = false, modifiedArgs, extras) => resolvePromise({
|
|
273
|
+
approved,
|
|
274
|
+
remember,
|
|
275
|
+
modifiedArgs,
|
|
276
|
+
...(extras?.rememberTier ? { rememberTier: extras.rememberTier } : {}),
|
|
277
|
+
...(extras?.reason ? { reason: extras.reason } : {}),
|
|
278
|
+
}),
|
|
257
279
|
openedAt: now,
|
|
258
280
|
requestedBy: resolveApprovalRequester(broker, request.callId, request.attribution) ?? undefined,
|
|
281
|
+
replyMode: isExecPrompt ? 'exec-answer' : undefined,
|
|
282
|
+
replyBuffer: isExecPrompt ? '' : undefined,
|
|
259
283
|
};
|
|
260
284
|
}
|
|
@@ -3,6 +3,8 @@ import { UIFactory } from '../renderer/ui-factory.ts';
|
|
|
3
3
|
import type { PermissionCategory, PermissionRequestAnalysis } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
4
4
|
import { buildPermissionApprovalBrief, getDisplayArg } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
5
5
|
import { DIFF_TONES, UI_TONES } from '../renderer/ui-primitives.ts';
|
|
6
|
+
import { renderDiffView } from '../renderer/diff-view.ts';
|
|
7
|
+
import { wrapText } from '../utils/terminal-width.ts';
|
|
6
8
|
import type { HunkSelectionState } from './hunk-selection.ts';
|
|
7
9
|
import { readSandboxAskAnnotation } from './sandbox-exec-gate.ts';
|
|
8
10
|
export { buildPendingPermissionExtras } from './hunk-selection.ts';
|
|
@@ -13,9 +15,28 @@ export type { PermissionPromptRequest, PermissionPromptDecision, PermissionReque
|
|
|
13
15
|
/** Visible hunk rows before a "+N more" trailer kicks in (Risk 3). */
|
|
14
16
|
const MAX_VISIBLE_HUNKS = 8;
|
|
15
17
|
|
|
16
|
-
/** Path rows shown before collapsing to a "N files: a, b, +K more" line (
|
|
18
|
+
/** Path rows shown before collapsing to a "N files: a, b, +K more" line (2a). */
|
|
17
19
|
const MAX_PATH_ROWS = 3;
|
|
18
20
|
|
|
21
|
+
/** Diff rows shown for a write/edit ask before the "+N more" trailer. */
|
|
22
|
+
const MAX_DIFF_LINES = 10;
|
|
23
|
+
|
|
24
|
+
/** Field-label column width shared by every " Label : value" row. */
|
|
25
|
+
const FIELD_PREFIX_LEN = 16;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* View-only state the card renders beyond the request itself: the typed-reply
|
|
29
|
+
* mode/draft (deny-with-reason, exec-prompt answer), the honest count of
|
|
30
|
+
* OTHER broker asks still waiting, and the terminal width getPromptHeight
|
|
31
|
+
* needs for wrap-dependent rows (createPromptLines takes width positionally).
|
|
32
|
+
*/
|
|
33
|
+
export interface PromptViewState {
|
|
34
|
+
readonly replyMode?: 'deny-reason' | 'exec-answer' | undefined;
|
|
35
|
+
readonly replyBuffer?: string | undefined;
|
|
36
|
+
readonly queueCount?: number | undefined;
|
|
37
|
+
readonly width?: number | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
/** Trailing filename for compact display of a long/absolute path. */
|
|
20
41
|
function baseName(p: string): string {
|
|
21
42
|
const cleaned = p.replace(/[/\\]+$/, '');
|
|
@@ -47,7 +68,7 @@ function collectField(arr: unknown, field: string): string[] {
|
|
|
47
68
|
/**
|
|
48
69
|
* The real display target(s) of a tool invocation — the paths/commands/urls a
|
|
49
70
|
* user needs to see, extracted from the actual arg shapes so a nested
|
|
50
|
-
* `{files:[{path}]}` no longer falls through to a raw JSON blob. (
|
|
71
|
+
* `{files:[{path}]}` no longer falls through to a raw JSON blob. (2a.)
|
|
51
72
|
* Returns [] when nothing recognisable is present (caller falls back).
|
|
52
73
|
*/
|
|
53
74
|
function cardTargets(args: Record<string, unknown>): string[] {
|
|
@@ -153,7 +174,7 @@ export class PermissionPromptUI {
|
|
|
153
174
|
/**
|
|
154
175
|
* A low-risk, project-local request is shown condensed (one summary line +
|
|
155
176
|
* choices) unless the user expanded it with `d`. High/critical risk, external
|
|
156
|
-
* scope, and hunk-selectable edits always show the full block. (
|
|
177
|
+
* scope, and hunk-selectable edits always show the full block. (2b.)
|
|
157
178
|
*/
|
|
158
179
|
private static isCondensed(
|
|
159
180
|
request: PermissionPromptRequest,
|
|
@@ -164,8 +185,11 @@ export class PermissionPromptUI {
|
|
|
164
185
|
// Shell execution, delegation and network requests always show the full
|
|
165
186
|
// card — their action semantics (command, side effects, host, checklist)
|
|
166
187
|
// warrant scrutiny even when the risk model rates them low. Only mundane
|
|
167
|
-
// low-risk filesystem reads/writes condense. (
|
|
188
|
+
// low-risk filesystem reads/writes condense. (2b.)
|
|
168
189
|
if (request.category === 'execute' || request.category === 'delegate') return false;
|
|
190
|
+
// Remember tiers are one-key choices that must be visible to be usable —
|
|
191
|
+
// a request carrying rememberOptions always shows the full card.
|
|
192
|
+
if (request.rememberOptions && request.rememberOptions.length > 0) return false;
|
|
169
193
|
const analysis = this.fallbackAnalysis(request);
|
|
170
194
|
if (analysis.riskLevel !== 'low') return false;
|
|
171
195
|
const scope = analysis.blastRadius;
|
|
@@ -197,32 +221,179 @@ export class PermissionPromptUI {
|
|
|
197
221
|
return `${prefix}${shown}${suffix}`;
|
|
198
222
|
}
|
|
199
223
|
|
|
224
|
+
/**
|
|
225
|
+
* The remember block's row texts: one row per SDK remember tier (numbered,
|
|
226
|
+
* most specific first) when the request carries rememberOptions, else the
|
|
227
|
+
* legacy single [A] session-preview row. Hunk mode has no whole-request
|
|
228
|
+
* remember choice at all. Shared by getPromptHeight and createPromptLines.
|
|
229
|
+
*/
|
|
230
|
+
private static rememberRowTexts(
|
|
231
|
+
request: PermissionPromptRequest,
|
|
232
|
+
hunkState: HunkSelectionState | undefined,
|
|
233
|
+
width: number,
|
|
234
|
+
): string[] {
|
|
235
|
+
if (hunkState) return [];
|
|
236
|
+
const options = request.rememberOptions ?? [];
|
|
237
|
+
if (options.length === 0) return [this.rememberPreviewText(request, width)];
|
|
238
|
+
const clamp = (s: string): string => (s.length > width ? `${s.slice(0, Math.max(0, width - 3))}...` : s);
|
|
239
|
+
return options.map((option, index) => {
|
|
240
|
+
const labelCol = index === 0 ? 'Remember ' : ' '.repeat(9);
|
|
241
|
+
return clamp(` ${labelCol}: [${index + 1}] ${option.label} — ${option.detail}`);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The FULL command of an execute ask, wrapped across as many rows as it
|
|
247
|
+
* needs — never truncated. Empty for non-execute asks and for asks without
|
|
248
|
+
* a string command. Shared by getPromptHeight and createPromptLines.
|
|
249
|
+
*/
|
|
250
|
+
private static commandRowTexts(request: PermissionPromptRequest, width: number): string[] {
|
|
251
|
+
if (request.category !== 'execute') return [];
|
|
252
|
+
// Exec-prompt asks render their command via execPromptRowTexts instead.
|
|
253
|
+
if (request.attribution?.kind === 'exec-prompt') return [];
|
|
254
|
+
const command = typeof request.args?.command === 'string'
|
|
255
|
+
? request.args.command
|
|
256
|
+
: typeof request.args?.cmd === 'string'
|
|
257
|
+
? request.args.cmd
|
|
258
|
+
: '';
|
|
259
|
+
if (command.length === 0) return [];
|
|
260
|
+
const budget = Math.max(10, width - FIELD_PREFIX_LEN);
|
|
261
|
+
const wrapped = command.split('\n').flatMap((line) => (line.length > 0 ? wrapText(line, budget) : ['']));
|
|
262
|
+
return wrapped.map((row, index) => (index === 0 ? ` Command : ${row}` : `${' '.repeat(FIELD_PREFIX_LEN - 1)}${row}`));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Unified-diff text lines for a write/edit ask (real colored rendering via
|
|
267
|
+
* renderDiffView), capped at MAX_DIFF_LINES with a context trailer naming
|
|
268
|
+
* how much is hidden. Null when the ask carries nothing diffable. Hunk-mode
|
|
269
|
+
* asks render the interactive hunk list instead. Shared by getPromptHeight
|
|
270
|
+
* and createPromptLines.
|
|
271
|
+
*/
|
|
272
|
+
private static writeDiffLines(request: PermissionPromptRequest, hunkState: HunkSelectionState | undefined): string[] | null {
|
|
273
|
+
if (hunkState || request.category !== 'write') return null;
|
|
274
|
+
const args = request.args ?? {};
|
|
275
|
+
const target = typeof args.path === 'string' ? args.path : typeof args.file === 'string' ? args.file : '';
|
|
276
|
+
const out: string[] = [];
|
|
277
|
+
if (typeof args.content === 'string') {
|
|
278
|
+
const contentLines = args.content.split('\n');
|
|
279
|
+
out.push(`+++ ${target || '(new content)'}`);
|
|
280
|
+
for (const line of contentLines.slice(0, MAX_DIFF_LINES)) out.push(`+${line}`);
|
|
281
|
+
if (contentLines.length > MAX_DIFF_LINES) out.push(` … +${contentLines.length - MAX_DIFF_LINES} more lines`);
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
if (Array.isArray(args.edits)) {
|
|
285
|
+
let budget = MAX_DIFF_LINES;
|
|
286
|
+
let hidden = 0;
|
|
287
|
+
for (const edit of args.edits) {
|
|
288
|
+
if (!edit || typeof edit !== 'object') continue;
|
|
289
|
+
const e = edit as { path?: unknown; find?: unknown; replace?: unknown };
|
|
290
|
+
if (typeof e.find !== 'string' || typeof e.replace !== 'string') continue;
|
|
291
|
+
const findLines = e.find.split('\n');
|
|
292
|
+
const replaceLines = e.replace.split('\n');
|
|
293
|
+
const need = 1 + findLines.length + replaceLines.length;
|
|
294
|
+
if (budget < need) {
|
|
295
|
+
hidden += 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
out.push(`@@ ${typeof e.path === 'string' ? e.path : target} @@`);
|
|
299
|
+
for (const line of findLines) out.push(`-${line}`);
|
|
300
|
+
for (const line of replaceLines) out.push(`+${line}`);
|
|
301
|
+
budget -= need;
|
|
302
|
+
}
|
|
303
|
+
if (out.length === 0) return null;
|
|
304
|
+
if (hidden > 0) out.push(` … +${hidden} more edits`);
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Rows for an exec-prompt ask (a running command waiting on stdin): the
|
|
312
|
+
* command and the terminal prompt text, both wrapped in full. Shared by
|
|
313
|
+
* getPromptHeight and createPromptLines.
|
|
314
|
+
*/
|
|
315
|
+
private static execPromptRowTexts(request: PermissionPromptRequest, width: number): string[] {
|
|
316
|
+
const attribution = request.attribution;
|
|
317
|
+
if (attribution?.kind !== 'exec-prompt') return [];
|
|
318
|
+
const budget = Math.max(10, width - FIELD_PREFIX_LEN);
|
|
319
|
+
const rows: string[] = [];
|
|
320
|
+
const pushWrapped = (label: string, text: string): void => {
|
|
321
|
+
const wrapped = text.split('\n').flatMap((line) => (line.length > 0 ? wrapText(line, budget) : ['']));
|
|
322
|
+
wrapped.forEach((row, index) => {
|
|
323
|
+
rows.push(index === 0 ? ` ${label.padEnd(9)}: ${row}` : `${' '.repeat(FIELD_PREFIX_LEN - 1)}${row}`);
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
if (typeof attribution.command === 'string' && attribution.command.length > 0) pushWrapped('Running', attribution.command);
|
|
327
|
+
if (typeof attribution.prompt === 'string' && attribution.prompt.length > 0) pushWrapped('Asks', attribution.prompt);
|
|
328
|
+
return rows;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** The typed-reply input row text (deny reason / exec answer), or null when no reply mode is active. */
|
|
332
|
+
private static replyRowText(view: PromptViewState | undefined, width: number): string | null {
|
|
333
|
+
if (!view?.replyMode) return null;
|
|
334
|
+
const label = view.replyMode === 'exec-answer' ? 'Answer ' : 'Reason ';
|
|
335
|
+
const draft = view.replyBuffer ?? '';
|
|
336
|
+
const budget = Math.max(6, width - FIELD_PREFIX_LEN - 1);
|
|
337
|
+
const shown = draft.length > budget ? `...${draft.slice(-(budget - 3))}` : draft;
|
|
338
|
+
return ` ${label}: ${shown}█`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Assemble the card's view state from the pending-permission slot and the
|
|
343
|
+
* broker's live queue (the honest count of OTHER pending asks — coalesced
|
|
344
|
+
* asks share one record, so they are never double-counted). One helper so
|
|
345
|
+
* main.ts passes identical state to getPromptHeight and createPromptLines.
|
|
346
|
+
*/
|
|
347
|
+
static promptViewState(
|
|
348
|
+
pending: { readonly callId: string; readonly replyMode?: 'deny-reason' | 'exec-answer' | undefined; readonly replyBuffer?: string | undefined },
|
|
349
|
+
width: number,
|
|
350
|
+
broker?: { listApprovals(limit?: number): ReadonlyArray<{ readonly callId: string; readonly status: string }> } | null,
|
|
351
|
+
): PromptViewState {
|
|
352
|
+
let queueCount = 0;
|
|
353
|
+
try {
|
|
354
|
+
queueCount = broker?.listApprovals().filter((record) => record.status === 'pending' && record.callId !== pending.callId).length ?? 0;
|
|
355
|
+
} catch {
|
|
356
|
+
queueCount = 0; // an unreadable broker never blocks the card
|
|
357
|
+
}
|
|
358
|
+
return { replyMode: pending.replyMode, replyBuffer: pending.replyBuffer, queueCount, width };
|
|
359
|
+
}
|
|
360
|
+
|
|
200
361
|
static getPromptHeight(
|
|
201
362
|
request: PermissionPromptRequest,
|
|
202
363
|
hunkState?: HunkSelectionState,
|
|
203
364
|
detailsExpanded = false,
|
|
204
365
|
requestedBy?: string,
|
|
366
|
+
view?: PromptViewState,
|
|
205
367
|
): number {
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
368
|
+
const width = view?.width ?? 80;
|
|
369
|
+
// Attribution line (only when known) + the remember block (tier rows when
|
|
370
|
+
// the request carries rememberOptions, else the legacy one-line preview)
|
|
371
|
+
// are added to BOTH card shapes; keep in sync with createPromptLines.
|
|
210
372
|
const attributionLines = requestedBy ? 1 : 0;
|
|
211
|
-
const
|
|
373
|
+
const rememberLines = this.rememberRowTexts(request, hunkState, width).length;
|
|
212
374
|
// Condensed low-risk card: top separator, title, [attribution], [preview],
|
|
213
375
|
// summary, choices, bottom separator — see createPromptLines' condensed branch.
|
|
214
|
-
if (this.isCondensed(request, hunkState, detailsExpanded)) return 5 + attributionLines +
|
|
376
|
+
if (this.isCondensed(request, hunkState, detailsExpanded)) return 5 + attributionLines + rememberLines;
|
|
215
377
|
const analysis = this.fallbackAnalysis(request);
|
|
216
378
|
const { annotation: judgmentAnnotation, rest: reasonsMinusJudgment } = extractModelJudgmentAnnotation(analysis.reasons);
|
|
217
379
|
const reasonLines = Math.min(2, Math.max(1, reasonsMinusJudgment.length));
|
|
218
380
|
const extraLines = (analysis.host ? 1 : 0) + (analysis.surface ? 1 : 0) + (analysis.sideEffects && analysis.sideEffects.length > 0 ? 1 : 0) + (readSandboxAskAnnotation(request) ? 1 : 0) + (judgmentAnnotation ? 1 : 0);
|
|
219
381
|
const hunkLines = hunkState ? hunkListRowCount(hunkState) : 0;
|
|
382
|
+
// Item-adopted blocks, each computed by the SAME helper createPromptLines
|
|
383
|
+
// renders from, so the two functions cannot drift: the full wrapped
|
|
384
|
+
// command (execute), the colored diff (write), the exec-prompt rows, and
|
|
385
|
+
// the typed-reply input row.
|
|
386
|
+
const commandLines = this.commandRowTexts(request, width).length;
|
|
387
|
+
const diffLines = this.writeDiffLines(request, hunkState)?.length ?? 0;
|
|
388
|
+
const execPromptLines = this.execPromptRowTexts(request, width).length;
|
|
389
|
+
const replyLines = this.replyRowText(view, width) ? 1 : 0;
|
|
220
390
|
// Base 12 counted a single arg line; the Path field now spans `pathRows`
|
|
221
391
|
// lines and the full card adds one raw-args row (2a reachability), so the
|
|
222
392
|
// arg allotment becomes pathRows + 1 → base 12 + pathRows (12 already
|
|
223
393
|
// included one of those). See createPromptLines' full branch.
|
|
224
394
|
const pathRows = this.pathRowTexts(this.resolvedTargets(request), 999).length;
|
|
225
|
-
return 12 + pathRows + reasonLines + extraLines + hunkLines + attributionLines +
|
|
395
|
+
return 12 + pathRows + reasonLines + extraLines + hunkLines + attributionLines + rememberLines
|
|
396
|
+
+ commandLines + diffLines + execPromptLines + replyLines;
|
|
226
397
|
}
|
|
227
398
|
|
|
228
399
|
/** Returns the key argument to display for a given tool invocation. */
|
|
@@ -258,6 +429,7 @@ export class PermissionPromptUI {
|
|
|
258
429
|
hunkState?: HunkSelectionState,
|
|
259
430
|
detailsExpanded = false,
|
|
260
431
|
requestedBy?: string,
|
|
432
|
+
view?: PromptViewState,
|
|
261
433
|
): Line[] {
|
|
262
434
|
const lines: Line[] = [];
|
|
263
435
|
const { tool, args, category } = request;
|
|
@@ -270,32 +442,40 @@ export class PermissionPromptUI {
|
|
|
270
442
|
const TEXT = '252';
|
|
271
443
|
const DIM = '244';
|
|
272
444
|
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
445
|
+
// Honest queue: how many OTHER broker asks are waiting behind this one.
|
|
446
|
+
// They surface in turn as each is answered; the count keeps that visible.
|
|
447
|
+
const queueSuffix = view?.queueCount && view.queueCount > 0
|
|
448
|
+
? ` — ${view.queueCount} more waiting`
|
|
449
|
+
: '';
|
|
450
|
+
|
|
451
|
+
// Attribution line (which agent/process is asking) and the remember block
|
|
452
|
+
// (numbered tier rows from the SDK's rememberOptions, or the legacy [A]
|
|
453
|
+
// session-preview row). Both are shared by the condensed and full cards;
|
|
454
|
+
// the remember block is suppressed for a hunk-selection prompt, which has
|
|
455
|
+
// no single whole-request remember key.
|
|
277
456
|
const pushAttribution = (): void => {
|
|
278
457
|
if (!requestedBy) return;
|
|
279
458
|
lines.push(UIFactory.stringToLine(` Requested by: ${requestedBy}`.padEnd(width), width, { fg: DIM }));
|
|
280
459
|
};
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
460
|
+
const pushRememberBlock = (): void => {
|
|
461
|
+
for (const rowText of this.rememberRowTexts(request, hunkState, width)) {
|
|
462
|
+
lines.push(UIFactory.stringToLine(rowText.padEnd(width), width, { fg: DIM }));
|
|
463
|
+
}
|
|
284
464
|
};
|
|
285
465
|
|
|
286
466
|
const maxArgLen = Math.max(10, width - 16);
|
|
287
467
|
const pathRows = this.pathRowTexts(this.resolvedTargets(request), maxArgLen);
|
|
288
468
|
|
|
289
469
|
// Condensed low-risk / project-local card: one summary line (verb → target
|
|
290
|
-
// (scope)) plus the choices. Full block is one `d` away. (
|
|
470
|
+
// (scope)) plus the choices. Full block is one `d` away. (2b.)
|
|
291
471
|
if (this.isCondensed(request, hunkState, detailsExpanded)) {
|
|
292
472
|
lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
|
|
293
|
-
lines.push(UIFactory.stringToLine(` [${label}] ${brief.title} `.padEnd(width), width, { fg: WARN, bold: true }));
|
|
473
|
+
lines.push(UIFactory.stringToLine(` [${label}] ${brief.title}${queueSuffix} `.padEnd(width), width, { fg: WARN, bold: true }));
|
|
294
474
|
pushAttribution();
|
|
295
475
|
const scopeText = analysis.blastRadius ? ` (${analysis.blastRadius})` : '';
|
|
296
476
|
const summaryLine = ` ${categoryVerb(category)} → ${pathRows[0]}${scopeText}`;
|
|
297
477
|
lines.push(UIFactory.stringToLine(summaryLine.padEnd(width), width, { fg: TEXT }));
|
|
298
|
-
|
|
478
|
+
pushRememberBlock();
|
|
299
479
|
lines.push(UIFactory.stringToLine(
|
|
300
480
|
` [Y] Allow once [A] Allow always (session) [N] Deny [d] details`.padEnd(width),
|
|
301
481
|
width, { fg: ACCENT, bold: true }));
|
|
@@ -306,9 +486,9 @@ export class PermissionPromptUI {
|
|
|
306
486
|
// Top separator
|
|
307
487
|
lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
|
|
308
488
|
|
|
309
|
-
// Title bar: category badge + title
|
|
489
|
+
// Title bar: category badge + title (+ honest waiting count)
|
|
310
490
|
const titleText = brief.title;
|
|
311
|
-
const titleLine = ` [${label}] ${titleText} `;
|
|
491
|
+
const titleLine = ` [${label}] ${titleText}${queueSuffix} `;
|
|
312
492
|
lines.push(UIFactory.stringToLine(titleLine.padEnd(width), width, { fg: WARN, bold: true }));
|
|
313
493
|
|
|
314
494
|
// Requester attribution (which agent/process raised this request).
|
|
@@ -320,12 +500,24 @@ export class PermissionPromptUI {
|
|
|
320
500
|
|
|
321
501
|
// Path/subject row(s): actual target path(s), one per line when few, a
|
|
322
502
|
// "N files: a, b, +K more" summary when many. Raw args move to the Args row
|
|
323
|
-
// below so this field never shows a truncated JSON blob. (
|
|
503
|
+
// below so this field never shows a truncated JSON blob. (2a.)
|
|
324
504
|
pathRows.forEach((rowText, i) => {
|
|
325
505
|
const labelCol = i === 0 ? brief.subjectLabel.padEnd(9) : ' '.repeat(9);
|
|
326
506
|
lines.push(UIFactory.stringToLine(` ${labelCol}: ${rowText}`.padEnd(width), width, { fg: TEXT }));
|
|
327
507
|
});
|
|
328
508
|
|
|
509
|
+
// The FULL command of an execute ask, wrapped — never truncated. The Path
|
|
510
|
+
// row above stays as the one-line subject; this block is the whole truth.
|
|
511
|
+
for (const rowText of this.commandRowTexts(request, width)) {
|
|
512
|
+
lines.push(UIFactory.stringToLine(rowText.padEnd(width), width, { fg: TEXT }));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Exec-prompt attribution: the running command and the terminal prompt it
|
|
516
|
+
// is stuck on, wrapped in full — the typed answer feeds this run's stdin.
|
|
517
|
+
for (const rowText of this.execPromptRowTexts(request, width)) {
|
|
518
|
+
lines.push(UIFactory.stringToLine(rowText.padEnd(width), width, { fg: WARN }));
|
|
519
|
+
}
|
|
520
|
+
|
|
329
521
|
// Working directory row
|
|
330
522
|
const cwd = request.workingDirectory ?? '(unknown)';
|
|
331
523
|
const maxCwdLen = Math.max(10, width - 16);
|
|
@@ -416,6 +608,13 @@ export class PermissionPromptUI {
|
|
|
416
608
|
// Blank spacer
|
|
417
609
|
lines.push(UIFactory.stringToLine(' '.repeat(width), width));
|
|
418
610
|
|
|
611
|
+
// Real colored diff for a write/edit ask (adds green, removals red, hunk
|
|
612
|
+
// headers blue — the same diff-view machinery the transcript uses).
|
|
613
|
+
const diffTextLines = this.writeDiffLines(request, hunkState);
|
|
614
|
+
if (diffTextLines) {
|
|
615
|
+
lines.push(...renderDiffView(diffTextLines.join('\n'), width));
|
|
616
|
+
}
|
|
617
|
+
|
|
419
618
|
if (hunkState) {
|
|
420
619
|
// Hunk list — see hunkListRowCount() for the row-count contract this
|
|
421
620
|
// block must match exactly (Risk 2: getPromptHeight/createPromptLines
|
|
@@ -450,15 +649,31 @@ export class PermissionPromptUI {
|
|
|
450
649
|
lines.push(UIFactory.stringToLine(trailerLine.padEnd(width), width, { fg: DIM }));
|
|
451
650
|
}
|
|
452
651
|
|
|
453
|
-
// Remember
|
|
454
|
-
|
|
652
|
+
// Remember block: numbered tier rows (rememberOptions) or the legacy [A]
|
|
653
|
+
// session preview (omitted for hunk mode).
|
|
654
|
+
pushRememberBlock();
|
|
655
|
+
|
|
656
|
+
// Typed-reply input row: the deny reason (Enter denies with it as
|
|
657
|
+
// feedback) or the exec-prompt answer (Enter feeds the running command).
|
|
658
|
+
const replyRow = this.replyRowText(view, width);
|
|
659
|
+
if (replyRow) {
|
|
660
|
+
lines.push(UIFactory.stringToLine(replyRow.padEnd(width), width, { fg: TEXT, bold: true }));
|
|
661
|
+
}
|
|
455
662
|
|
|
456
663
|
// Choices row. A condensable card shown in full is the expanded form, so it
|
|
457
664
|
// offers `[d]` to collapse back; other full cards omit the details toggle.
|
|
458
665
|
const collapseHint = this.isCondensed(request, hunkState, false) ? ' [d] hide details' : '';
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
666
|
+
const tierCount = hunkState ? 0 : (request.rememberOptions?.length ?? 0);
|
|
667
|
+
const rememberHint = tierCount > 0
|
|
668
|
+
? ` [1${tierCount > 1 ? `-${tierCount}` : ''}] Allow + remember`
|
|
669
|
+
: ' [A] Allow always (session)';
|
|
670
|
+
const choicesLine = view?.replyMode === 'exec-answer'
|
|
671
|
+
? ` [Enter] Send answer [Esc] ${((view.replyBuffer ?? '').length > 0) ? 'Clear' : 'Decline'} [Ctrl+C] Abort turn`
|
|
672
|
+
: view?.replyMode === 'deny-reason'
|
|
673
|
+
? ` [Enter] Deny with this reason [Esc] Back [Ctrl+C] Abort turn`
|
|
674
|
+
: hunkState
|
|
675
|
+
? ` [j/k] Navigate [Space] Toggle [A] All [Enter] Apply selected [N] Deny`
|
|
676
|
+
: ` [Y] Allow once${rememberHint} [N] Deny type a reason to deny${collapseHint}`;
|
|
462
677
|
lines.push(UIFactory.stringToLine(choicesLine.padEnd(width), width, { fg: ACCENT, bold: true }));
|
|
463
678
|
|
|
464
679
|
// Bottom separator
|