@pellux/goodvibes-tui 1.17.0 → 1.19.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 +208 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5025 -1636
- package/package.json +7 -7
- package/src/cli/endpoints.ts +50 -4
- package/src/cli/help.ts +29 -2
- package/src/cli/management-commands.ts +99 -23
- package/src/cli/management.ts +0 -2
- package/src/cli/service-posture.ts +5 -3
- package/src/cli/status.ts +27 -6
- package/src/cli/surface-command.ts +29 -15
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-rendering.ts +14 -5
- package/src/core/memory-diagnostics-gateway.ts +99 -0
- package/src/core/memory-provenance.ts +67 -0
- package/src/core/memory-status.ts +118 -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 +21 -0
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +39 -0
- package/src/core/turn-event-wiring.ts +6 -0
- package/src/core/voice-provision-gateway.ts +202 -0
- package/src/core/voice-provision-status.ts +197 -0
- package/src/daemon/cli.ts +127 -60
- package/src/input/command-registry.ts +21 -0
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/devices-runtime.ts +109 -0
- package/src/input/commands/experience-runtime.ts +25 -4
- 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 +15 -1
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +44 -25
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -0
- package/src/input/config-modal-types.ts +11 -0
- package/src/input/config-modal.ts +65 -2
- package/src/input/handler-shortcuts.ts +23 -0
- package/src/input/keybindings.ts +19 -1
- package/src/input/settings-modal-data.ts +31 -37
- package/src/input/settings-modal-types.ts +5 -2
- package/src/input/tts-settings-actions.ts +27 -8
- package/src/input/voice-local-settings.ts +52 -0
- package/src/main.ts +42 -42
- package/src/panels/builtin/operations.ts +54 -1
- package/src/panels/builtin/shared.ts +12 -0
- package/src/panels/builtin-modals.ts +49 -32
- package/src/panels/diff-panel.ts +34 -1
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +120 -4
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +60 -44
- package/src/panels/fleet-read-model.ts +34 -3
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-stop.ts +38 -2
- package/src/panels/fleet-tabs.ts +2 -2
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/memory-modal.ts +149 -5
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/security-modal.ts +6 -0
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/settings-modal-helpers.ts +9 -0
- package/src/renderer/settings-modal.ts +4 -2
- package/src/renderer/shell-surface.ts +3 -0
- package/src/renderer/tool-call.ts +12 -6
- package/src/renderer/ui-factory.ts +32 -0
- package/src/runtime/bootstrap-command-context.ts +3 -1
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +6 -6
- package/src/runtime/bootstrap-shell.ts +7 -0
- package/src/runtime/code-index-services.ts +15 -1
- package/src/runtime/durability-services.ts +36 -1
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/knowledge-services.ts +94 -0
- package/src/runtime/legacy-daemon-migration.ts +38 -6
- package/src/runtime/legacy-daemon-reconcile.ts +439 -0
- package/src/runtime/memory-governance-services.ts +79 -0
- package/src/runtime/notification-dispatch.ts +28 -0
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +105 -107
- package/src/runtime/ui-services.ts +3 -0
- package/src/runtime/voice-setup-services.ts +104 -0
- package/src/shell/blocking-input.ts +21 -141
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/version.ts +11 -2
|
@@ -18,6 +18,7 @@ export type SettingsCategory =
|
|
|
18
18
|
| 'tools'
|
|
19
19
|
| 'helper'
|
|
20
20
|
| 'tts'
|
|
21
|
+
| 'voice'
|
|
21
22
|
| 'service'
|
|
22
23
|
| 'daemon'
|
|
23
24
|
| 'controlPlane'
|
|
@@ -46,6 +47,8 @@ export type SettingsCategory =
|
|
|
46
47
|
| 'notifications'
|
|
47
48
|
| 'release'
|
|
48
49
|
| 'update'
|
|
50
|
+
| 'power'
|
|
51
|
+
| 'memory'
|
|
49
52
|
| 'danger';
|
|
50
53
|
|
|
51
54
|
export type SettingsFocusPane = 'categories' | 'settings';
|
|
@@ -55,11 +58,11 @@ export const SETTINGS_CATEGORY_GROUPS: ReadonlyArray<{
|
|
|
55
58
|
readonly categories: readonly SettingsCategory[];
|
|
56
59
|
}> = [
|
|
57
60
|
{ label: 'Interface', categories: ['display', 'ui', 'behavior', 'notifications', 'permissions', 'policy', 'security'] },
|
|
58
|
-
{ label: 'AI Routing', categories: ['provider', 'pricing', 'subscriptions', 'helper', 'tools', 'tts'] },
|
|
61
|
+
{ label: 'AI Routing', categories: ['provider', 'pricing', 'subscriptions', 'helper', 'tools', 'tts', 'voice'] },
|
|
59
62
|
{ label: 'Service & Network', categories: ['service', 'daemon', 'network', 'controlPlane', 'httpListener', 'web', 'relay'] },
|
|
60
63
|
{ label: 'Surfaces & Cloud', categories: ['surfaces', 'integrations', 'mcp', 'cloudflare'] },
|
|
61
64
|
{ label: 'Automation', categories: ['batch', 'automation', 'checkin', 'watchers', 'orchestration', 'planner', 'wrfc'] },
|
|
62
|
-
{ label: 'Runtime & Data', categories: ['storage', 'atRest', 'sandbox', 'fetch', 'agents', 'runtime', 'cache', 'telemetry', 'diagnostics', 'learning'] },
|
|
65
|
+
{ label: 'Runtime & Data', categories: ['storage', 'atRest', 'sandbox', 'fetch', 'agents', 'runtime', 'power', 'memory', 'cache', 'telemetry', 'diagnostics', 'learning'] },
|
|
63
66
|
{ label: 'Advanced', categories: ['release', 'update', 'danger'] },
|
|
64
67
|
];
|
|
65
68
|
|
|
@@ -2,6 +2,7 @@ import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
2
2
|
import type { CommandContext } from './command-registry.ts';
|
|
3
3
|
import type { SelectionItem } from './selection-modal.ts';
|
|
4
4
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
|
+
import { localVoiceSetupOffer } from './voice-local-settings.ts';
|
|
5
6
|
|
|
6
7
|
function getStreamingTtsProviders(ctx: CommandContext): Array<{ id: string; label: string; capabilities: readonly string[] }> {
|
|
7
8
|
const registry = ctx.platform.voiceProviderRegistry;
|
|
@@ -28,14 +29,32 @@ export function openTtsProviderPicker(ctx: CommandContext): boolean {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const current = String(ctx.platform.configManager.get('tts.provider') ?? '').trim();
|
|
31
|
-
const items: SelectionItem[] = providers.map((provider) =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
const items: SelectionItem[] = providers.map((provider) => {
|
|
33
|
+
// The managed local engine renders beside ElevenLabs; when it is not yet
|
|
34
|
+
// provisioned, its detail line offers the size-labeled one-act install so
|
|
35
|
+
// the operator sees the download cost before committing to /voice setup.
|
|
36
|
+
if (provider.id === 'local') {
|
|
37
|
+
const offer = localVoiceSetupOffer((key) => ctx.platform.configManager.get(key as Parameters<typeof ctx.platform.configManager.get>[0]));
|
|
38
|
+
if (!offer.provisioned) {
|
|
39
|
+
return {
|
|
40
|
+
id: provider.id,
|
|
41
|
+
label: provider.label,
|
|
42
|
+
detail: provider.id === current ? `${offer.detail} (current)` : offer.detail,
|
|
43
|
+
category: 'streaming TTS providers',
|
|
44
|
+
primaryAction: 'select',
|
|
45
|
+
actions: offer.actions,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
id: provider.id,
|
|
51
|
+
label: provider.label,
|
|
52
|
+
detail: provider.id === current ? `${provider.id} (current)` : provider.id,
|
|
53
|
+
category: 'streaming TTS providers',
|
|
54
|
+
primaryAction: 'select',
|
|
55
|
+
actions: '[Enter] set provider',
|
|
56
|
+
};
|
|
57
|
+
});
|
|
39
58
|
|
|
40
59
|
ctx.openSelection('Choose TTS Provider', items, { preSelectId: current, allowSearch: true }, (result) => {
|
|
41
60
|
if (!result) return;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// voice-local-settings.ts — the managed local-voice setup affordance the voice
|
|
3
|
+
// settings surfaces render beside ElevenLabs.
|
|
4
|
+
//
|
|
5
|
+
// The local engine registers in the voice provider registry regardless of
|
|
6
|
+
// whether it is provisioned yet (its status is honestly "unconfigured", never
|
|
7
|
+
// an error). When it is NOT provisioned, the settings/provider-picker surface
|
|
8
|
+
// offers the one-act install with its download size declared up front — the
|
|
9
|
+
// size is computed synchronously from the pinned piper manifest
|
|
10
|
+
// (piperProvisionBytes) so no daemon round-trip is needed just to label the
|
|
11
|
+
// offer. Platforms with no pinned managed build say so honestly.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
import { currentVoicePlatform, piperProvisionBytes } from '@pellux/goodvibes-sdk/platform/voice';
|
|
15
|
+
import { formatVoiceBytes } from '../core/voice-provision-status.ts';
|
|
16
|
+
|
|
17
|
+
export interface LocalVoiceSetupOffer {
|
|
18
|
+
/** A pinned managed piper build exists for the current platform. */
|
|
19
|
+
readonly supported: boolean;
|
|
20
|
+
/** The local TTS engine + model config keys are set (managed install applied or user-set). */
|
|
21
|
+
readonly provisioned: boolean;
|
|
22
|
+
/** Human size label for the one-act install ("~85 MB" or an honest unavailable note). */
|
|
23
|
+
readonly sizeLabel: string;
|
|
24
|
+
/** The detail line the provider picker renders for the local engine. */
|
|
25
|
+
readonly detail: string;
|
|
26
|
+
/** The picker action hint (advertises /voice setup only where it can actually provision). */
|
|
27
|
+
readonly actions: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compute the managed local-voice setup offer from config, with the download
|
|
32
|
+
* size declared up front. `provisioned` reflects whether the local TTS engine
|
|
33
|
+
* binary + model keys are set; an unprovisioned-but-supported platform offers
|
|
34
|
+
* the size-labeled /voice setup one-act.
|
|
35
|
+
*/
|
|
36
|
+
export function localVoiceSetupOffer(configGet: (key: string) => unknown): LocalVoiceSetupOffer {
|
|
37
|
+
const platform = currentVoicePlatform();
|
|
38
|
+
const bytes = platform ? piperProvisionBytes(platform) : null;
|
|
39
|
+
const supported = bytes !== null && bytes !== undefined;
|
|
40
|
+
const ttsBinary = String(configGet('voice.local.ttsBinary') ?? '').trim();
|
|
41
|
+
const ttsModel = String(configGet('voice.local.ttsModelPath') ?? '').trim();
|
|
42
|
+
const provisioned = ttsBinary.length > 0 && ttsModel.length > 0;
|
|
43
|
+
const sizeLabel = supported ? `~${formatVoiceBytes(bytes)}` : 'unavailable on this platform';
|
|
44
|
+
|
|
45
|
+
let detail: string;
|
|
46
|
+
if (!supported) detail = 'local voice: no managed build for this platform (set voice.local.* by hand to use it)';
|
|
47
|
+
else if (provisioned) detail = 'local voice: configured (managed engine set)';
|
|
48
|
+
else detail = `local voice: not set up — run /voice setup (${sizeLabel})`;
|
|
49
|
+
|
|
50
|
+
const actions = supported && !provisioned ? '[Enter] select · /voice setup to provision' : '[Enter] set provider';
|
|
51
|
+
return { supported, provisioned, sizeLabel, detail, actions };
|
|
52
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -39,10 +39,10 @@ import { createSessionContinuityHintsBuilder } from './runtime/session-continuit
|
|
|
39
39
|
import { resolveWebSurfaceUrl } from '@pellux/goodvibes-sdk/platform/runtime/feature-announcements';
|
|
40
40
|
import { readLastSessionPointer, writeRecoveryFile } from '@/runtime/index.ts';
|
|
41
41
|
import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
|
|
42
|
-
import {
|
|
42
|
+
import { autoRestoreRecoverySession, createPersistRecoverySnapshot, createReopenRecoveryPanels, handleErrorAffordanceKey } from './shell/recovery-input-helpers.ts';
|
|
43
43
|
import { wireShellUiOpeners } from './shell/ui-openers.ts';
|
|
44
44
|
import { deriveComposerState } from './core/composer-state.ts';
|
|
45
|
-
import { buildPersistedSessionContext,
|
|
45
|
+
import { buildPersistedSessionContext, getReturnContextMode, maybeAssistReturnContextSummary } from '@/runtime/index.ts';
|
|
46
46
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
47
47
|
import { prepareShellCliRuntime } from './cli/entrypoint.ts';
|
|
48
48
|
import { applyInitialTuiCliState } from './cli/tui-startup.ts';
|
|
@@ -57,7 +57,7 @@ import { buildCommandArgsHint } from './input/command-args-hint.ts';
|
|
|
57
57
|
import { summarizeRunningAgents } from './renderer/process-summary.ts';
|
|
58
58
|
import { footerFleetCost } from './panels/fleet-read-model.ts';
|
|
59
59
|
import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
60
|
-
import { wireStreamEventMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
|
|
60
|
+
import { wireStreamEventMetrics, createStreamMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
|
|
61
61
|
import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
|
|
62
62
|
import { buildContextStatusHint } from './renderer/context-status-hint.ts';
|
|
63
63
|
import { isEffectiveDangerMode } from './config/index.ts';
|
|
@@ -67,6 +67,9 @@ import { createSessionAutoTitler } from './core/session-auto-titler.ts';
|
|
|
67
67
|
import { makeComposerEditorOpener } from './input/composer-editor.ts';
|
|
68
68
|
import { evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
69
69
|
import { createCancelGeneration } from './core/turn-cancellation.ts';
|
|
70
|
+
import { wireInteractionSeams, createMemoryProvenanceUi } from './runtime/interaction-seams.ts';
|
|
71
|
+
import { createPowerChipSource } from './core/power-chip-source.ts';
|
|
72
|
+
import { fetchDaemonPowerState, installKeepAwakeRemoteForward } from './runtime/power-keepawake-remote.ts';
|
|
70
73
|
import { wrapRequestPermissionWithAlert } from './core/approval-alert.ts';
|
|
71
74
|
import { createTerminalNotifier } from './core/terminal-notifier.ts';
|
|
72
75
|
import { setPanelFrameRequester } from './panels/base-panel.ts';
|
|
@@ -160,17 +163,7 @@ async function main() {
|
|
|
160
163
|
// Cached from the overlay-aware clamp the renderer computes (conversation-layout) each frame so scroll() clamps against exactly what is displayed, not a footer estimate.
|
|
161
164
|
let lastMaxScroll: number | null = null;
|
|
162
165
|
// Stream and tool-timer state; mutated by wireStreamEventMetrics handlers, read during render.
|
|
163
|
-
const streamMetrics: StreamMetrics =
|
|
164
|
-
startTime: 0,
|
|
165
|
-
deltaCount: 0,
|
|
166
|
-
tokenSpeed: 0,
|
|
167
|
-
ttftMs: undefined,
|
|
168
|
-
ttftRecorded: false,
|
|
169
|
-
activeToolStartedAtMs: undefined,
|
|
170
|
-
activeToolName: undefined,
|
|
171
|
-
lastDeltaAtMs: undefined, stallEpisode: 0,
|
|
172
|
-
reconnectAttempt: undefined, reconnectMaxAttempts: undefined,
|
|
173
|
-
};
|
|
166
|
+
const streamMetrics: StreamMetrics = createStreamMetrics();
|
|
174
167
|
|
|
175
168
|
const getPromptContentWidth = () => computePromptContentWidth(stdout.columns);
|
|
176
169
|
|
|
@@ -203,9 +196,13 @@ async function main() {
|
|
|
203
196
|
const unsubs: Array<() => void> = [];
|
|
204
197
|
let recoveryInterval: ReturnType<typeof setInterval> | null = null;
|
|
205
198
|
let stopSpokenOutputForExit: (() => Promise<void>) | null = null;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
199
|
+
// The optional "used N memories" provenance chip (default OFF) — see interaction-seams.ts.
|
|
200
|
+
const memoryProvenanceUi = createMemoryProvenanceUi({ render: () => render(), memorySpine: ctx.services.memorySpine });
|
|
201
|
+
// Topology-aware keep-awake: the chip renders the DAEMON's state in adopted-external mode (power-chip-source.ts) and every toggle is forwarded to that daemon (power-keepawake-remote.ts) so keep-awake survives the TUI closing in BOTH topologies.
|
|
202
|
+
const isExternalDaemon = () => uiServices.platform.externalServices?.inspect()?.daemonStatus?.mode === 'external';
|
|
203
|
+
const powerChipSource = createPowerChipSource({ powerManager: ctx.services.powerManager, render: () => render(), isExternalDaemon,
|
|
204
|
+
onPowerEvent: (cb) => uiServices.events.ops.on('OPS_POWER_STATE_CHANGED', cb as never), fetchDaemonPowerState: () => fetchDaemonPowerState({ configManager, homeDirectory, isExternalDaemon }) });
|
|
205
|
+
unsubs.push(powerChipSource.stop, installKeepAwakeRemoteForward({ configManager, homeDirectory, isExternalDaemon }));
|
|
209
206
|
|
|
210
207
|
const lifecycle = installProcessLifecycle({
|
|
211
208
|
stdin,
|
|
@@ -329,6 +326,10 @@ async function main() {
|
|
|
329
326
|
// Late-patched: bootstrap.ts populates uiServices.platform.externalServices AFTER commandContext is built.
|
|
330
327
|
commandContext.platform.externalServices = uiServices.platform.externalServices;
|
|
331
328
|
commandContext.cancelGeneration = cancelGeneration;
|
|
329
|
+
wireInteractionSeams(commandContext, {
|
|
330
|
+
orchestrator, powerManager: ctx.services.powerManager, readPowerSurface: () => powerChipSource.get(), render: () => render(), notify: (m) => systemMessageRouter.high(m),
|
|
331
|
+
getActiveToolCallId: () => streamMetrics.activeToolCallId, toggleMemoryProvenance: () => memoryProvenanceUi.toggle(),
|
|
332
|
+
});
|
|
332
333
|
commandContext.isGenerating = () => orchestrator.isThinking;
|
|
333
334
|
commandContext.jumpToBookmark = jumpToBookmark; commandContext.scrollToLine = scrollToLine;
|
|
334
335
|
commandContext.clearScreen = () => {
|
|
@@ -373,7 +374,7 @@ async function main() {
|
|
|
373
374
|
surfaceRegistry: ctx.services.surfaceRegistry,
|
|
374
375
|
subscriptionManager: ctx.services.subscriptionManager,
|
|
375
376
|
secretsManager: ctx.services.secretsManager,
|
|
376
|
-
tokenAuditor: ctx.services.tokenAuditor,
|
|
377
|
+
pairingTokens: ctx.services.pairingTokens, tokenAuditor: ctx.services.tokenAuditor,
|
|
377
378
|
replayEngine: ctx.services.replayEngine,
|
|
378
379
|
webhookNotifier: ctx.services.webhookNotifier,
|
|
379
380
|
policyRuntimeState: ctx.services.policyRuntimeState,
|
|
@@ -496,6 +497,8 @@ async function main() {
|
|
|
496
497
|
// Cross-surface spine posture segment (adopted-daemon mode only).
|
|
497
498
|
sessionSpineStatus: (() => { const s = uiServices.platform.externalServices?.inspect(); return s?.sessionSpineActive && s.sessionSpineStatus && s.sessionSpineStatus !== 'unknown' ? s.sessionSpineStatus : undefined; })(), runningAgentCount, runningProcessCount,
|
|
498
499
|
webSurfaceUrl: configManager.get('web.enabled') ? resolveWebSurfaceUrl(configManager) : undefined,
|
|
500
|
+
// Always-visible "sleep disabled" chip — topology-aware: the DAEMON's state in adopted-external mode, the in-process manager otherwise (power-chip-source.ts).
|
|
501
|
+
powerKeepAwake: powerChipSource.get().keepAwake,
|
|
499
502
|
// Composer must not read as focused while the panel/process indicator owns keyboard focus.
|
|
500
503
|
promptFocused: !input.panelFocused && !input.indicatorFocused,
|
|
501
504
|
indicatorFocused: input.indicatorFocused,
|
|
@@ -586,7 +589,7 @@ async function main() {
|
|
|
586
589
|
viewport.push(...thinking);
|
|
587
590
|
// Live tool timer: render the currently executing tool row with ticking elapsed.
|
|
588
591
|
if (streamMetrics.activeToolName !== undefined && streamMetrics.activeToolStartedAtMs !== undefined) {
|
|
589
|
-
const liveToolCall = { id: 'live', name: streamMetrics.activeToolName, arguments: {} };
|
|
592
|
+
const liveToolCall = { id: streamMetrics.activeToolCallId ?? 'live', name: streamMetrics.activeToolName, arguments: {} };
|
|
590
593
|
viewport.push(...renderToolCallBlock(liveToolCall, 'executing', undefined, conversationWidth, undefined, undefined, undefined, streamMetrics.activeToolStartedAtMs));
|
|
591
594
|
}
|
|
592
595
|
}
|
|
@@ -595,9 +598,8 @@ async function main() {
|
|
|
595
598
|
viewport.push(...PermissionPromptUI.createPromptLines(conversationWidth, pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded, pendingPermission.requestedBy, PermissionPromptUI.promptViewState(pendingPermission, conversationWidth, approvalBroker)));
|
|
596
599
|
}
|
|
597
600
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
});
|
|
601
|
+
viewport.push(...UIFactory.createQueuedMessageList(conversationWidth, orchestrator.listQueuedMessages()));
|
|
602
|
+
viewport.push(...memoryProvenanceUi.renderChip(conversationWidth, configManager));
|
|
601
603
|
|
|
602
604
|
viewport = applyConversationOverlays(viewport, {
|
|
603
605
|
input,
|
|
@@ -686,6 +688,8 @@ async function main() {
|
|
|
686
688
|
});
|
|
687
689
|
unsubs.push(...turnUnsubs);
|
|
688
690
|
|
|
691
|
+
unsubs.push(uiServices.events.turns.on('TURN_COMPLETED', (evt) => memoryProvenanceUi.onTurnCompleted(evt)));
|
|
692
|
+
|
|
689
693
|
// Stable turn context for failover retry — set in submitInput, read by retryTurn.
|
|
690
694
|
let retryCtx: { count: number; text: string; content?: ContentPart[]; opts?: Parameters<typeof orchestrator.handleUserInput>[2] } | null = null;
|
|
691
695
|
// One-key retry affordance, active right after a user-visible TURN_ERROR: 'r' re-submits on the
|
|
@@ -735,15 +739,10 @@ async function main() {
|
|
|
735
739
|
stdin.on('data', (raw: string) => {
|
|
736
740
|
const data = themeProbe.filterInput(raw); if (data.length === 0) return;
|
|
737
741
|
const blocking = handleBlockingShellInput({
|
|
738
|
-
data, pendingPermission,
|
|
742
|
+
data, pendingPermission, render,
|
|
739
743
|
abortTurn: () => orchestrator.abort(),
|
|
740
|
-
...createRecoveryFileOps(() => recoverySource, { homeDirectory }),
|
|
741
|
-
homeDirectory, sessionId: runtime.sessionId,
|
|
742
|
-
persistSnapshot: createPersistRecoverySnapshot({ sessionManager: ctx.services.sessionManager, runtime, conversation }),
|
|
743
|
-
reopenPanels: createReopenRecoveryPanels({ panelManager, render }),
|
|
744
744
|
});
|
|
745
745
|
pendingPermission = blocking.pendingPermission;
|
|
746
|
-
recoveryPending = blocking.recoveryPending;
|
|
747
746
|
if (blocking.handled) {
|
|
748
747
|
return;
|
|
749
748
|
}
|
|
@@ -765,26 +764,27 @@ async function main() {
|
|
|
765
764
|
process.on('unhandledRejection', unhandledRejectionHandler);
|
|
766
765
|
stdout.on('resize', resizeHandler);
|
|
767
766
|
|
|
767
|
+
// --- Silent crash-recovery restore (no prompt/banner) ---
|
|
768
|
+
// Restore the newest crash snapshot in place, replay any post-snapshot
|
|
769
|
+
// journal turns, reopen its panels, and emit a single one-line receipt. The
|
|
770
|
+
// SDK scopes both the load and the follow-up delete to that snapshot's own
|
|
771
|
+
// session id, so a concurrent session's snapshot is never touched. Runs
|
|
772
|
+
// before the initial render so the restored content shows immediately.
|
|
773
|
+
autoRestoreRecoverySession({
|
|
774
|
+
workingDirectory: workingDir,
|
|
775
|
+
homeDirectory,
|
|
776
|
+
conversation,
|
|
777
|
+
persistSnapshot: createPersistRecoverySnapshot({ sessionManager: ctx.services.sessionManager, runtime, conversation }),
|
|
778
|
+
reopenPanels: createReopenRecoveryPanels({ panelManager, render }),
|
|
779
|
+
systemMessageRouter,
|
|
780
|
+
});
|
|
781
|
+
|
|
768
782
|
// Initial render
|
|
769
783
|
conversation.rebuildHistory();
|
|
770
784
|
render();
|
|
771
785
|
|
|
772
|
-
// --- Crash recovery check (also checks the preserve-on-dismiss sibling; see recovery-input-helpers.ts) ---
|
|
773
|
-
const recoveryInfo = resolveStartupRecoveryInfo({ workingDirectory: workingDir, homeDirectory });
|
|
774
|
-
if (recoveryInfo) {
|
|
775
|
-
recoverySource = recoveryInfo.source;
|
|
776
|
-
systemMessageRouter.high(`[Recovery] Found unsaved session from ${new Date(recoveryInfo.timestamp).toLocaleString()}. Title: "${recoveryInfo.title}". Press Ctrl+R to restore, Esc to discard, or start typing to ignore it.`);
|
|
777
|
-
for (const line of formatReturnContextForDisplay(recoveryInfo.returnContext)) {
|
|
778
|
-
systemMessageRouter.low(`[Recovery] ${line}`);
|
|
779
|
-
}
|
|
780
|
-
render();
|
|
781
|
-
recoveryPending = true;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
786
|
// --- Auto-save to recovery file every 60s ---
|
|
785
787
|
recoveryInterval = setInterval(() => {
|
|
786
|
-
// Skip while an earlier recovery prompt is unresolved (avoids racing preserve-on-dismiss).
|
|
787
|
-
if (recoveryPending) return;
|
|
788
788
|
const snapshot = conversation.toJSON() as { messages: Array<import('./core/conversation.ts').ConversationMessageSnapshot> };
|
|
789
789
|
const persisted = buildPersistedSessionContext(snapshot.messages, conversation.getTitleSource(), buildSessionContinuityHints());
|
|
790
790
|
writeRecoveryFile(
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { PanelManager } from '../panel-manager.ts';
|
|
2
2
|
import { FleetPanel } from '../fleet-panel.ts';
|
|
3
3
|
import { createFleetReadModel } from '../fleet-read-model.ts';
|
|
4
|
+
import { FleetActs, type FleetDiffSurface } from '../fleet-acts.ts';
|
|
5
|
+
import { createFleetGateway } from '../fleet-gateway.ts';
|
|
6
|
+
import { FleetSpawn, createAcpSpawnGateway } from '../fleet-spawn.ts';
|
|
7
|
+
import { DiffPanel } from '../diff-panel.ts';
|
|
4
8
|
import { LocalAuthPanel } from '../local-auth-panel.ts';
|
|
5
9
|
import { NotificationsPanel } from '../notifications-panel.ts';
|
|
6
10
|
import type { ResolvedBuiltinPanelDeps } from './shared.ts';
|
|
@@ -43,6 +47,55 @@ export function registerOperationsPanels(manager: PanelManager, deps: ResolvedBu
|
|
|
43
47
|
// to the honest COMMUNICATION_CONSUMED steer-ack signal (fleet-read-model.ts).
|
|
44
48
|
const fleetReadModel = createFleetReadModel(ui.runtime.processRegistry, ui.runtime.runtimeBus);
|
|
45
49
|
|
|
50
|
+
// The Fleet panel's waiting-on-human acts (pick / conflict / discard) drive
|
|
51
|
+
// gateway verbs over the same operator invoke path the command layer uses, and
|
|
52
|
+
// reuse the shared DiffPanel (candidate diffs + the pick confirm overlay) and
|
|
53
|
+
// the CI fix-session jump affordance. Built here so the panel stays thin.
|
|
54
|
+
const fleetNotify = deps.fleetActsNotify ?? (() => {});
|
|
55
|
+
const armFixSessionAttach = deps.armFixSessionAttach ?? (() => {});
|
|
56
|
+
const diffSurface: FleetDiffSurface = {
|
|
57
|
+
show: (_title, unifiedDiff) => {
|
|
58
|
+
let panel = manager.getAllOpen().find((p) => p.id === 'diff');
|
|
59
|
+
if (!panel) { try { panel = manager.open('diff'); } catch { return; } }
|
|
60
|
+
manager.activateById('diff');
|
|
61
|
+
if (!manager.isVisible()) manager.show();
|
|
62
|
+
manager.focusPanels();
|
|
63
|
+
(panel as DiffPanel).loadRawDiff(unifiedDiff);
|
|
64
|
+
},
|
|
65
|
+
armConfirm: (opts) => {
|
|
66
|
+
let panel = manager.getAllOpen().find((p) => p.id === 'diff');
|
|
67
|
+
if (!panel) { try { panel = manager.open('diff'); } catch { return; } }
|
|
68
|
+
manager.activateById('diff');
|
|
69
|
+
if (!manager.isVisible()) manager.show();
|
|
70
|
+
manager.focusPanels();
|
|
71
|
+
(panel as DiffPanel).confirmOverlay.arm(opts);
|
|
72
|
+
},
|
|
73
|
+
close: () => { manager.close('diff'); },
|
|
74
|
+
};
|
|
75
|
+
const fleetActs = new FleetActs({
|
|
76
|
+
resolveGateway: () => createFleetGateway({
|
|
77
|
+
configManager: deps.configManager,
|
|
78
|
+
homeDirectory: ui.environment.shellPaths.homeDirectory,
|
|
79
|
+
armFixSessionAttach,
|
|
80
|
+
}),
|
|
81
|
+
diffSurface,
|
|
82
|
+
notify: fleetNotify,
|
|
83
|
+
markDirty: deps.requestRender ?? (() => {}),
|
|
84
|
+
findNode: (nodeId: string) => fleetReadModel.getSnapshot().rows.find((r) => r.node.id === nodeId)?.node ?? null,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// The ACP spawn affordance ('n'): host a discovered third-party agent as an
|
|
88
|
+
// acp-agent row over the same operator invoke path the acts use.
|
|
89
|
+
const fleetSpawn = new FleetSpawn({
|
|
90
|
+
resolveGateway: () => createAcpSpawnGateway({
|
|
91
|
+
configManager: deps.configManager,
|
|
92
|
+
homeDirectory: ui.environment.shellPaths.homeDirectory,
|
|
93
|
+
}),
|
|
94
|
+
currentDirectory: () => ui.environment.shellPaths.workingDirectory,
|
|
95
|
+
notify: fleetNotify,
|
|
96
|
+
markDirty: deps.requestRender ?? (() => {}),
|
|
97
|
+
});
|
|
98
|
+
|
|
46
99
|
manager.registerType({
|
|
47
100
|
id: 'fleet',
|
|
48
101
|
name: 'Fleet',
|
|
@@ -63,7 +116,7 @@ export function registerOperationsPanels(manager: PanelManager, deps: ResolvedBu
|
|
|
63
116
|
resolveSessionLogPath: (agentId: string) => ui.environment.shellPaths.resolveProjectPath('tui', 'sessions', `${agentId}.jsonl`),
|
|
64
117
|
// Queue a message for a live in-process agent/wrfc-subtask member.
|
|
65
118
|
steer: (id: string, text: string) => fleetReadModel.steer(id, text),
|
|
66
|
-
}, deps.configManager),
|
|
119
|
+
}, deps.configManager, fleetActs, fleetSpawn),
|
|
67
120
|
});
|
|
68
121
|
|
|
69
122
|
// Compat: '/panel open <id>' (and any saved layout/muscle memory) for
|
|
@@ -150,6 +150,18 @@ export interface BuiltinPanelDeps {
|
|
|
150
150
|
* cross-panel navigation instead of printing a "/panel open …" signpost.
|
|
151
151
|
*/
|
|
152
152
|
openPanel?: (panelId: string) => void;
|
|
153
|
+
/**
|
|
154
|
+
* Surface a Fleet act result/receipt/error to the operator (late-bound to the
|
|
155
|
+
* command context's print, which the Fleet pick/conflict/discard acts use for
|
|
156
|
+
* their receipts — the same conversation sink the command flow prints to).
|
|
157
|
+
*/
|
|
158
|
+
fleetActsNotify?: (message: string) => void;
|
|
159
|
+
/**
|
|
160
|
+
* Hand a spawned session id to the shared one-key jump/attach affordance (the
|
|
161
|
+
* CI fix-session machinery). Late-bound: main.ts patches it onto the command
|
|
162
|
+
* context after bootstrap; the Fleet conflict-resolve act reuses it verbatim.
|
|
163
|
+
*/
|
|
164
|
+
armFixSessionAttach?: (sessionId: string) => void;
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
export type ResolvedBuiltinPanelDeps = Omit<
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { networkInterfaces } from 'node:os';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
1
|
import type { PanelManager } from './panel-manager.ts';
|
|
4
2
|
import { requireUiServices, type ResolvedBuiltinPanelDeps } from './builtin/shared.ts';
|
|
5
3
|
import type { ConfigModalSurface, ConfigModalView } from '../input/config-modal-types.ts';
|
|
6
4
|
import { createProviderRuntimeInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
7
5
|
import { createRuntimeProviderApi } from '@/runtime/index.ts';
|
|
8
6
|
import { copyToClipboard } from '../utils/clipboard.ts';
|
|
9
|
-
import {
|
|
7
|
+
import { ensurePublicBaseUrl } from '../core/pairing-origin.ts';
|
|
8
|
+
import { availablePairingOffers, mintPairingHandoff, defaultPairingTokenName } from '../core/pairing-handoff.ts';
|
|
9
|
+
import { probePairingTailscale, runPairingTailscaleServe } from '../core/pairing-tailscale-gateway.ts';
|
|
10
10
|
// ── Providers & Connectivity + Security subset ────────────────────────────────
|
|
11
11
|
import { createServicesModalSurface } from './modals/services-modal.ts';
|
|
12
12
|
import { createSubscriptionModalSurface } from './modals/subscription-modal.ts';
|
|
@@ -24,9 +24,11 @@ import { createSecurityModalSurface } from './modals/security-modal.ts';
|
|
|
24
24
|
import { createPolicyModalSurface } from './modals/policy-modal.ts';
|
|
25
25
|
import { createKnowledgeModalSurface } from './modals/knowledge-modal.ts';
|
|
26
26
|
import { createMemoryModalSurface, type MemoryModalDeps } from './modals/memory-modal.ts';
|
|
27
|
+
import { createMemoryConsolidationGateway } from './memory-consolidation-gateway.ts';
|
|
27
28
|
import { createWorkPlanModalSurface } from './modals/work-plan-modal.ts';
|
|
28
29
|
import { createKeybindingsModalSurface } from './modals/keybindings-modal.ts';
|
|
29
30
|
import { createPairingModalSurface, type PairingModalConnectionInfo } from './modals/pairing-modal.ts';
|
|
31
|
+
import { createDevicesModalSurface } from './modals/devices-modal.ts';
|
|
30
32
|
import { createPlanningModalSurface } from './modals/planning-modal.ts';
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -131,6 +133,13 @@ export function registerBuiltinModals(manager: PanelManager, deps: ResolvedBuilt
|
|
|
131
133
|
|
|
132
134
|
manager.registerModalSurface(createMemoryModalSurface({
|
|
133
135
|
memoryRegistry: deps.memoryRegistry as MemoryModalDeps['memoryRegistry'],
|
|
136
|
+
// Lazily resolved — a fresh factory call per Proposals-tab fetch, exactly
|
|
137
|
+
// like the Fleet gateway (fleet-gateway.ts) — so a daemon that comes up
|
|
138
|
+
// AFTER this session started is still reachable on the next refresh.
|
|
139
|
+
resolveConsolidationGateway: () => createMemoryConsolidationGateway({
|
|
140
|
+
configManager: deps.configManager,
|
|
141
|
+
homeDirectory: ui.environment.shellPaths.homeDirectory,
|
|
142
|
+
}),
|
|
134
143
|
}));
|
|
135
144
|
manager.registerModalRedirect('memory', 'memory-modal');
|
|
136
145
|
|
|
@@ -144,13 +153,24 @@ export function registerBuiltinModals(manager: PanelManager, deps: ResolvedBuilt
|
|
|
144
153
|
}));
|
|
145
154
|
manager.registerModalRedirect('docs', 'keybindings-modal');
|
|
146
155
|
|
|
156
|
+
const tailscaleGatewayDeps = { configManager: deps.configManager, homeDirectory: ui.environment.shellPaths.homeDirectory };
|
|
147
157
|
manager.registerModalSurface(createPairingModalSurface({
|
|
148
158
|
getConnectionInfo: () => buildPairingConnectionInfo(deps),
|
|
149
159
|
controlPlaneReadModel: ui.readModels.controlPlane,
|
|
150
160
|
copyToClipboard,
|
|
161
|
+
probeTailscale: () => probePairingTailscale(tailscaleGatewayDeps),
|
|
162
|
+
runTailscaleServe: () => runPairingTailscaleServe(tailscaleGatewayDeps),
|
|
151
163
|
}));
|
|
152
164
|
manager.registerModalRedirect('qr-code', 'pairing-modal');
|
|
153
165
|
|
|
166
|
+
// Paired-device management (settings security domain): list/rename/revoke/
|
|
167
|
+
// migrate-shared over the same per-device token store the pairing QR mints into.
|
|
168
|
+
manager.registerModalSurface(createDevicesModalSurface({
|
|
169
|
+
pairingTokens: ui.platform.pairingTokens,
|
|
170
|
+
copyToClipboard,
|
|
171
|
+
}));
|
|
172
|
+
manager.registerModalRedirect('devices', 'devices-modal');
|
|
173
|
+
|
|
154
174
|
manager.registerModalSurface(createPlanningModalSurface({
|
|
155
175
|
service: deps.projectPlanningService,
|
|
156
176
|
projectId: deps.projectPlanningProjectId,
|
|
@@ -171,15 +191,33 @@ export function registerBuiltinModals(manager: PanelManager, deps: ResolvedBuilt
|
|
|
171
191
|
* Mirrors the retired QrPanel factory's construction.
|
|
172
192
|
*/
|
|
173
193
|
function buildPairingConnectionInfo(deps: ResolvedBuiltinPanelDeps): PairingModalConnectionInfo | null {
|
|
174
|
-
if (!deps.daemonHomeDir) return null;
|
|
175
194
|
try {
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
195
|
+
const ui = requireUiServices(deps);
|
|
196
|
+
const configManager = deps.configManager;
|
|
197
|
+
// Freeze a stable web origin once (never clobbering a user-set value).
|
|
198
|
+
const webOrigin = ensurePublicBaseUrl(configManager);
|
|
199
|
+
const offers = availablePairingOffers({
|
|
200
|
+
relayEnabled: configManager.get('relay.enabled') === true,
|
|
201
|
+
// The TUI composition always wires a step-up service ⇒ the passkey offer is
|
|
202
|
+
// presentable; the web app declines it if the device has no authenticator.
|
|
203
|
+
stepUpAvailable: true,
|
|
204
|
+
});
|
|
205
|
+
// Mint a fresh per-device token and encode the canonical `#pair=` deep link —
|
|
206
|
+
// the same mint the pairing.handoff.create verb performs, no raw JSON blob.
|
|
207
|
+
const handoff = mintPairingHandoff({
|
|
208
|
+
pairingTokens: ui.platform.pairingTokens,
|
|
209
|
+
name: defaultPairingTokenName(),
|
|
210
|
+
offers,
|
|
211
|
+
webOrigin: webOrigin.origin,
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
url: webOrigin.origin,
|
|
215
|
+
token: handoff.token.token,
|
|
216
|
+
tokenName: handoff.token.name,
|
|
217
|
+
deepLink: handoff.deepLink ?? handoff.fragment,
|
|
218
|
+
offers: handoff.offers,
|
|
219
|
+
posture: handoff.posture,
|
|
220
|
+
};
|
|
183
221
|
} catch {
|
|
184
222
|
return null;
|
|
185
223
|
}
|
|
@@ -195,24 +233,3 @@ function unwiredSurface(name: string, title: string, reason: string): ConfigModa
|
|
|
195
233
|
};
|
|
196
234
|
}
|
|
197
235
|
|
|
198
|
-
function getLocalNetworkIp(): string {
|
|
199
|
-
const nets = networkInterfaces();
|
|
200
|
-
for (const name of Object.keys(nets)) {
|
|
201
|
-
for (const net of nets[name] ?? []) {
|
|
202
|
-
if (net.family === 'IPv4' && !net.internal) return net.address;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return 'localhost';
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function readBootstrapPassword(credentialPath: string): string | undefined {
|
|
209
|
-
try {
|
|
210
|
-
const content = readFileSync(credentialPath, 'utf-8');
|
|
211
|
-
for (const line of content.split('\n')) {
|
|
212
|
-
if (line.startsWith('password=')) return line.slice('password='.length).trim();
|
|
213
|
-
}
|
|
214
|
-
} catch {
|
|
215
|
-
// credential file may not exist yet
|
|
216
|
-
}
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
package/src/panels/diff-panel.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { Line } from '../types/grid.ts';
|
|
6
6
|
import { createStyledCell, createEmptyLine } from '../types/grid.ts';
|
|
7
7
|
import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
8
|
-
import { GitService } from '@pellux/goodvibes-sdk/platform/git';
|
|
8
|
+
import { GitService, type StructuredDiff, type StructuredDiffFile } from '@pellux/goodvibes-sdk/platform/git';
|
|
9
9
|
import { BasePanel } from './base-panel.ts';
|
|
10
10
|
import { UI_TONES, DIFF_TONES } from '../renderer/ui-primitives.ts';
|
|
11
11
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
@@ -199,6 +199,19 @@ function extractDiffFilePath(trimmed: string): string {
|
|
|
199
199
|
return 'unknown';
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/** Reconstruct one file's unified-diff text from a StructuredDiffFile (header + hunks, prefixes re-attached; no size cap). */
|
|
203
|
+
function reconstructStructuredFile(file: StructuredDiffFile): string {
|
|
204
|
+
const out: string[] = [...file.headerLines];
|
|
205
|
+
for (const hunk of file.hunks) {
|
|
206
|
+
out.push(hunk.header);
|
|
207
|
+
for (const line of hunk.lines) {
|
|
208
|
+
const prefix = line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ' ';
|
|
209
|
+
out.push(`${prefix}${line.text}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return out.join('\n');
|
|
213
|
+
}
|
|
214
|
+
|
|
202
215
|
function splitIntoDiffEntries(raw: string): DiffEntry[] {
|
|
203
216
|
const entries: DiffEntry[] = [];
|
|
204
217
|
// Split on "diff --git" / "diff --cc" / "diff --combined" lines
|
|
@@ -306,6 +319,26 @@ export class DiffPanel extends BasePanel {
|
|
|
306
319
|
this.markDirty();
|
|
307
320
|
}
|
|
308
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Ingest a StructuredDiff (GitService.diffStructured — the FULL, uncapped
|
|
324
|
+
* working-tree diff). Each file becomes one DiffEntry (unified text
|
|
325
|
+
* reconstructed) so the existing tabs/renderer work unchanged; the old
|
|
326
|
+
* 4,000-char slice is gone and a diff of any length renders complete.
|
|
327
|
+
*/
|
|
328
|
+
loadStructuredDiff(diff: StructuredDiff): void {
|
|
329
|
+
this.entries = diff.files.map((file) => {
|
|
330
|
+
const raw = reconstructStructuredFile(file);
|
|
331
|
+
return {
|
|
332
|
+
filePath: file.newPath ?? file.oldPath ?? 'unknown',
|
|
333
|
+
raw,
|
|
334
|
+
lines: parseDiff(raw),
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
this.selectedFile = 0;
|
|
338
|
+
this.scrollOffset = 0;
|
|
339
|
+
this.markDirty();
|
|
340
|
+
}
|
|
341
|
+
|
|
309
342
|
/**
|
|
310
343
|
* Defensive gate mirroring GitPanel's own notGitRepo messaging — short-
|
|
311
344
|
* circuits before any of this panel's self-load git spawns run, instead of
|