@pellux/goodvibes-tui 1.13.1 → 1.14.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 +96 -0
- package/README.md +6 -6
- package/bin/goodvibes +6 -0
- package/bin/goodvibes-daemon +6 -0
- package/bin/launcher-support.js +34 -1
- package/docs/foundation-artifacts/operator-contract.json +8076 -2047
- package/package.json +15 -11
- package/scripts/postinstall.js +73 -48
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completion.ts +1 -1
- package/src/cli/completions/generate.ts +15 -3
- package/src/cli/doctor.ts +385 -0
- package/src/cli/entrypoint.ts +72 -1
- package/src/cli/help.ts +29 -9
- package/src/cli/hooks-command.ts +74 -0
- package/src/cli/hooks-report.ts +167 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/management.ts +13 -1
- package/src/cli/parser.ts +10 -2
- package/src/cli/plugin-bundles-command.ts +228 -0
- package/src/cli/plugin-command.ts +179 -0
- package/src/cli/service-posture.ts +10 -0
- package/src/cli/status.ts +275 -2
- package/src/cli/tui-startup.ts +119 -1
- package/src/cli/types.ts +3 -1
- package/src/config/index.ts +16 -0
- package/src/config/tui-extension-settings.ts +169 -0
- package/src/core/approval-alert.ts +15 -2
- package/src/core/compaction-receipt.ts +127 -0
- package/src/core/model-routing-chip.ts +82 -0
- package/src/core/permission-mode.ts +89 -0
- package/src/core/rewind-receipt.ts +83 -0
- package/src/core/rewind-turn-anchors.ts +160 -0
- package/src/core/scriptable-statusline.ts +141 -0
- package/src/core/session-auto-titler.ts +110 -0
- package/src/core/stream-event-wiring.ts +20 -0
- package/src/core/system-message-router.ts +1 -1
- package/src/core/terminal-notifier.ts +102 -0
- package/src/core/turn-event-wiring.ts +80 -1
- package/src/input/command-reference.ts +96 -0
- package/src/input/command-registry.ts +25 -0
- package/src/input/commands/channel-pairing.ts +245 -0
- package/src/input/commands/channel-runtime.ts +152 -6
- package/src/input/commands/checkin-runtime.ts +89 -0
- package/src/input/commands/checkpoint-runtime.ts +7 -179
- package/src/input/commands/ci-runtime.ts +207 -0
- package/src/input/commands/cost-attribution-format.ts +52 -0
- package/src/input/commands/cost-runtime.ts +44 -4
- package/src/input/commands/editor-runtime.ts +22 -0
- package/src/input/commands/flags-runtime.ts +328 -0
- package/src/input/commands/health-metrics-format.ts +67 -0
- package/src/input/commands/health-runtime.ts +55 -1
- package/src/input/commands/memory.ts +23 -0
- package/src/input/commands/operator-rpc.ts +95 -0
- package/src/input/commands/palette-runtime.ts +22 -0
- package/src/input/commands/permissions-provenance.ts +264 -0
- package/src/input/commands/permissions-runtime.ts +21 -0
- package/src/input/commands/planning-runtime.ts +28 -1
- package/src/input/commands/platform-access-runtime.ts +2 -97
- package/src/input/commands/principals-runtime.ts +212 -0
- package/src/input/commands/product-runtime.ts +43 -3
- package/src/input/commands/recall-files-config.ts +54 -0
- package/src/input/commands/recall-files-git.ts +57 -0
- package/src/input/commands/recall-files.ts +185 -0
- package/src/input/commands/recall-query.ts +8 -3
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/recall-shared.ts +13 -0
- package/src/input/commands/relay-runtime.ts +86 -0
- package/src/input/commands/review-runtime.ts +160 -0
- package/src/input/commands/rewind-runtime.ts +362 -0
- package/src/input/commands/schedule-nl.ts +147 -0
- package/src/input/commands/schedule-runtime.ts +29 -10
- package/src/input/commands/secret-runtime.ts +55 -0
- package/src/input/commands/session-content.ts +19 -8
- package/src/input/commands/session-workflow.ts +10 -0
- package/src/input/commands/update-runtime.ts +363 -0
- package/src/input/commands/workstream-attempt-dependents.ts +103 -0
- package/src/input/commands/workstream-attempts.ts +244 -0
- package/src/input/commands/workstream-runtime.ts +164 -26
- package/src/input/commands/worktree-runtime.ts +56 -2
- package/src/input/commands.ts +135 -80
- package/src/input/composer-capture.ts +67 -0
- package/src/input/composer-editor.ts +134 -0
- package/src/input/concealed-input.ts +94 -0
- package/src/input/exec-env-scrub-config.ts +84 -0
- package/src/input/feature-unit-layout.ts +142 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed-routes.ts +17 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-modal-routes.ts +5 -2
- package/src/input/handler-shortcuts.ts +37 -0
- package/src/input/handler.ts +24 -2
- package/src/input/keybindings.ts +11 -2
- package/src/input/onboarding/onboarding-feature-units.ts +536 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
- package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
- package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
- package/src/input/relay-settings-descriptions.ts +35 -0
- package/src/input/sandbox-exec-config.ts +107 -0
- package/src/input/settings-modal-adjustment.ts +4 -7
- package/src/input/settings-modal-data.ts +178 -8
- package/src/input/settings-modal-mutations.ts +41 -13
- package/src/input/settings-modal-types.ts +42 -5
- package/src/input/settings-modal.ts +67 -15
- package/src/input/submission-router.ts +4 -1
- package/src/input/worktree-setup-config.ts +93 -0
- package/src/main.ts +43 -43
- package/src/panels/builtin/development.ts +14 -0
- package/src/panels/diff-review-model.ts +189 -0
- package/src/panels/diff-review-panel.ts +395 -0
- package/src/panels/fleet-panel-format.ts +114 -2
- package/src/panels/fleet-panel.ts +65 -78
- package/src/panels/fleet-read-model.ts +91 -11
- package/src/panels/fleet-stop.ts +19 -4
- package/src/panels/modals/memory-modal.ts +11 -2
- package/src/permissions/hunk-selection.ts +86 -5
- package/src/permissions/prompt.ts +108 -7
- package/src/permissions/sandbox-exec-gate.ts +162 -0
- package/src/renderer/settings-modal-helpers.ts +21 -1
- package/src/renderer/settings-modal.ts +70 -44
- package/src/renderer/shell-surface.ts +23 -1
- package/src/renderer/system-message.ts +4 -0
- package/src/renderer/tool-result-summary.ts +95 -5
- package/src/renderer/ui-factory.ts +22 -0
- package/src/runtime/bootstrap-command-context.ts +15 -1
- package/src/runtime/bootstrap-command-parts.ts +14 -2
- package/src/runtime/bootstrap-core.ts +38 -38
- package/src/runtime/bootstrap-notifier-sync.ts +41 -0
- package/src/runtime/bootstrap-shell.ts +8 -0
- package/src/runtime/bootstrap.ts +21 -23
- package/src/runtime/context-accounting-source.ts +103 -0
- package/src/runtime/conversation-rewind-port.ts +130 -0
- package/src/runtime/fleet-needs-input-push.ts +70 -0
- package/src/runtime/install-self-check-startup.ts +36 -0
- package/src/runtime/install-self-check.ts +149 -0
- package/src/runtime/onboarding/fast-path.ts +93 -0
- package/src/runtime/process-lifecycle.ts +32 -24
- package/src/runtime/relay-reachability-bridge.ts +53 -0
- package/src/runtime/release-artifacts.ts +79 -0
- package/src/runtime/services.ts +33 -33
- package/src/runtime/trust/workspace-registration.ts +184 -0
- package/src/runtime/trust/workspace-trust.ts +202 -0
- package/src/runtime/ui-services.ts +2 -1
- package/src/runtime/update-check.ts +113 -0
- package/src/runtime/workspace-checkpointing.ts +32 -0
- package/src/runtime/workstream-attempts-validation.ts +77 -0
- package/src/runtime/workstream-draft-edits.ts +95 -0
- package/src/runtime/workstream-draft-store.ts +101 -0
- package/src/runtime/workstream-draft-types.ts +42 -0
- package/src/runtime/workstream-services.ts +70 -39
- package/src/shell/blocking-input.ts +29 -0
- package/src/shell/ui-openers.ts +65 -0
- package/src/verification/verification-ledger.ts +56 -2
- package/src/version.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
- package/src/runtime/gateway-verbs.ts +0 -47
- package/src/runtime/render-scheduler.ts +0 -90
|
@@ -25,6 +25,9 @@ import {
|
|
|
25
25
|
} from './settings-modal-secrets.ts';
|
|
26
26
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
27
27
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
28
|
+
import { isWorktreeSetupListConfigKey, parseWorktreeSetupListInput } from './worktree-setup-config.ts';
|
|
29
|
+
import { isSandboxExecListConfigKey, parseSandboxExecListInput } from './sandbox-exec-config.ts';
|
|
30
|
+
import { isExecEnvScrubAllowlistConfigKey, parseExecEnvScrubAllowlistInput } from './exec-env-scrub-config.ts';
|
|
28
31
|
|
|
29
32
|
import {
|
|
30
33
|
SETTINGS_CATEGORIES,
|
|
@@ -48,6 +51,7 @@ import {
|
|
|
48
51
|
import { getSettingLabel } from '../renderer/settings-modal-helpers.ts';
|
|
49
52
|
import {
|
|
50
53
|
applySettingValue,
|
|
54
|
+
applyFlagState,
|
|
51
55
|
type SettingAppliedCallback,
|
|
52
56
|
} from './settings-modal-mutations.ts';
|
|
53
57
|
import {
|
|
@@ -56,7 +60,6 @@ import {
|
|
|
56
60
|
} from './settings-modal-activation.ts';
|
|
57
61
|
import {
|
|
58
62
|
adjustSelected as _adjustSelected,
|
|
59
|
-
toggleSelectedFlag as _toggleSelectedFlag,
|
|
60
63
|
} from './settings-modal-adjustment.ts';
|
|
61
64
|
import {
|
|
62
65
|
resetSelected as _resetSelected,
|
|
@@ -203,7 +206,7 @@ export class SettingsModal {
|
|
|
203
206
|
this.serviceRegistry = serviceRegistry;
|
|
204
207
|
this.mcpRegistry = mcpRegistry ?? null;
|
|
205
208
|
this.onSettingApplied = options?.onSettingApplied ?? null;
|
|
206
|
-
this.groups = buildSettingGroups(configManager);
|
|
209
|
+
this.groups = buildSettingGroups(configManager, featureFlagManager);
|
|
207
210
|
this.flagEntries = buildFlagEntries(featureFlagManager);
|
|
208
211
|
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
209
212
|
this.subscriptionEntries = buildSubscriptionEntries(subscriptionManager, serviceRegistry);
|
|
@@ -378,11 +381,14 @@ export class SettingsModal {
|
|
|
378
381
|
return items[Math.max(0, Math.min(items.length - 1, this.selectedIndex))] ?? null;
|
|
379
382
|
}
|
|
380
383
|
|
|
381
|
-
/**
|
|
384
|
+
/**
|
|
385
|
+
* Get the flag entry for the currently selected feature-unit header row, or
|
|
386
|
+
* null when the selected row is a plain config setting. Feature-unit headers
|
|
387
|
+
* now live across topical categories (not a single flags tab), so this reads
|
|
388
|
+
* the selected SettingEntry's attached flag rather than a separate list.
|
|
389
|
+
*/
|
|
382
390
|
getSelectedFlag(): FlagEntry | null {
|
|
383
|
-
|
|
384
|
-
if (this.flagEntries.length === 0) return null;
|
|
385
|
-
return this.flagEntries[Math.max(0, Math.min(this.flagEntries.length - 1, this.selectedIndex))] ?? null;
|
|
391
|
+
return this.getSelected()?.flag ?? null;
|
|
386
392
|
}
|
|
387
393
|
|
|
388
394
|
getSelectedMcp(): McpEntry | null {
|
|
@@ -493,11 +499,32 @@ export class SettingsModal {
|
|
|
493
499
|
* only (require restart). runtimeToggleable flags toggle immediately.
|
|
494
500
|
*/
|
|
495
501
|
toggleSelectedFlag(): void {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
502
|
+
const entry = this.getSelected();
|
|
503
|
+
if (!entry?.flag) return;
|
|
504
|
+
if (entry.flag.state === 'killed') return;
|
|
505
|
+
this._toggleFlagValue(entry.setting.key as ConfigKey, entry.flag.state !== 'enabled');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Toggle the feature flag behind a `featureFlags.<id>` header row. Finds the
|
|
510
|
+
* header entry, applies the desired state through the flag manager (runtime +
|
|
511
|
+
* persisted override), and refreshes the header row's value/default marker in
|
|
512
|
+
* place. Killed flags and no-op toggles are ignored.
|
|
513
|
+
*/
|
|
514
|
+
private _toggleFlagValue(key: ConfigKey, value: unknown): void {
|
|
515
|
+
if (!this.featureFlagManager || !this.configManager) return;
|
|
516
|
+
let target: SettingEntry | null = null;
|
|
517
|
+
for (const entries of this.groups.values()) {
|
|
518
|
+
const candidate = entries.find((e) => e.setting.key === key && e.flag);
|
|
519
|
+
if (candidate) { target = candidate; break; }
|
|
520
|
+
}
|
|
521
|
+
if (!target?.flag) return;
|
|
522
|
+
if (target.flag.state === 'killed') return;
|
|
523
|
+
const desired = value ? 'enabled' : 'disabled';
|
|
524
|
+
if (target.flag.state === desired) return;
|
|
525
|
+
applyFlagState(target.flag, desired, this.featureFlagManager, this.configManager);
|
|
526
|
+
target.currentValue = target.flag.state === 'enabled';
|
|
527
|
+
target.isDefault = target.flag.state === target.flag.flag.defaultState;
|
|
501
528
|
}
|
|
502
529
|
|
|
503
530
|
/**
|
|
@@ -565,7 +592,20 @@ export class SettingsModal {
|
|
|
565
592
|
return false;
|
|
566
593
|
}
|
|
567
594
|
|
|
568
|
-
if (
|
|
595
|
+
if (isWorktreeSetupListConfigKey(setting.key)) {
|
|
596
|
+
// Comma-separated display/edit convention for the array-backed
|
|
597
|
+
// worktree.setup.* keys — see worktree-setup-config.ts.
|
|
598
|
+
this._setValue(setting.key, parseWorktreeSetupListInput(this.editBuffer));
|
|
599
|
+
} else if (isSandboxExecListConfigKey(setting.key)) {
|
|
600
|
+
// Same comma-separated convention for the array-backed
|
|
601
|
+
// sandbox.egressAllowlist / sandbox.workspaceWritable keys — see
|
|
602
|
+
// sandbox-exec-config.ts.
|
|
603
|
+
this._setValue(setting.key, parseSandboxExecListInput(this.editBuffer));
|
|
604
|
+
} else if (isExecEnvScrubAllowlistConfigKey(setting.key)) {
|
|
605
|
+
// Same comma-separated convention for permissions.execEnvScrubAllowlist
|
|
606
|
+
// — see exec-env-scrub-config.ts.
|
|
607
|
+
this._setValue(setting.key, parseExecEnvScrubAllowlistInput(this.editBuffer));
|
|
608
|
+
} else if (setting.type === 'string' && isSecretConfigKey(setting.key)) {
|
|
569
609
|
setSecretBackedSettingValue({
|
|
570
610
|
key: setting.key,
|
|
571
611
|
value: String(parsed ?? ''),
|
|
@@ -656,11 +696,14 @@ export class SettingsModal {
|
|
|
656
696
|
}
|
|
657
697
|
}
|
|
658
698
|
|
|
659
|
-
/**
|
|
699
|
+
/**
|
|
700
|
+
* Returns [] for the mcp/subscriptions categories (which render their own
|
|
701
|
+
* entry types). The 'flags' category now flows through the normal group path
|
|
702
|
+
* — it holds the no-config feature-unit toggles (Advanced Features).
|
|
703
|
+
*/
|
|
660
704
|
private _currentItems(): SettingEntry[] {
|
|
661
705
|
if (
|
|
662
|
-
this.currentCategory === '
|
|
663
|
-
|| this.currentCategory === 'mcp'
|
|
706
|
+
this.currentCategory === 'mcp'
|
|
664
707
|
|| this.currentCategory === 'subscriptions'
|
|
665
708
|
) return [];
|
|
666
709
|
const items = this.groups.get(this.currentCategory) ?? [];
|
|
@@ -673,6 +716,15 @@ export class SettingsModal {
|
|
|
673
716
|
private _setValue(key: ConfigKey, value: unknown): void {
|
|
674
717
|
if (!this.configManager) return;
|
|
675
718
|
|
|
719
|
+
// Feature-unit toggle headers write to `featureFlags.<id>`; route those to
|
|
720
|
+
// the feature-flag manager (runtime toggle + persisted override) instead of
|
|
721
|
+
// a plain config write. Reset/adjust/activate all funnel a boolean value
|
|
722
|
+
// here (true = enabled), so this is the single flag-toggle chokepoint.
|
|
723
|
+
if (typeof key === 'string' && key.startsWith('featureFlags.')) {
|
|
724
|
+
this._toggleFlagValue(key, value);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
676
728
|
const callback: SettingAppliedCallback | null = this.onSettingApplied
|
|
677
729
|
? (change) => this.onSettingApplied!(change)
|
|
678
730
|
: null;
|
|
@@ -7,7 +7,10 @@ export interface SubmissionRouterInput {
|
|
|
7
7
|
readonly hasAttachments?: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// The project-planning command (renamed from /plan to /project-plan when /plan
|
|
11
|
+
// became the plan-mode toggle) takes a free-form goal, so it keeps the distinct
|
|
12
|
+
// 'plan' composer intent/label. The /plan mode-toggle is a plain slash command.
|
|
13
|
+
const PLAN_COMMANDS = new Set(['project-plan', 'planning']);
|
|
11
14
|
const REVIEW_COMMANDS = new Set(['review']);
|
|
12
15
|
const PANEL_COMMANDS = new Set(['panel']);
|
|
13
16
|
const ORCHESTRATION_COMMANDS = new Set([
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-setup-config.ts — TUI-local synthetic settings entries for
|
|
3
|
+
* worktree.setup.commands / worktree.setup.carryOverGlobs.
|
|
4
|
+
*
|
|
5
|
+
* Neither key is in the SDK's ConfigKey union: the SDK's cold-start worktree
|
|
6
|
+
* setup (platform/runtime/worktree/setup.ts, resolveWorktreeSetupConfig)
|
|
7
|
+
* reads them off a generic `get(key: string)` rather than the typed
|
|
8
|
+
* CONFIG_SCHEMA, so without this file they are invisible to /config and the
|
|
9
|
+
* settings modal even though they fully control what runs when a fresh
|
|
10
|
+
* isolated worktree is provisioned (setup commands, then untracked-file
|
|
11
|
+
* carry-over). Same rationale as the other synthetic settings in
|
|
12
|
+
* settings-modal-data.ts (tts.speed, behavior.notifyAfterSeconds, etc).
|
|
13
|
+
*
|
|
14
|
+
* Both keys hold a JSON array of strings on disk. The settings modal's
|
|
15
|
+
* inline editor is a single-line text field, so — matching the SDK's own
|
|
16
|
+
* convention for controlPlane.cors.allowedOrigins — they are displayed and
|
|
17
|
+
* edited as a comma-separated list and parsed back into an array on commit
|
|
18
|
+
* (see isWorktreeSetupListConfigKey's use in settings-modal.ts#commitEdit).
|
|
19
|
+
*/
|
|
20
|
+
import type { ConfigKey, ConfigManager, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
|
|
21
|
+
import type { SettingEntry } from './settings-modal-types.ts';
|
|
22
|
+
|
|
23
|
+
export const WORKTREE_SETUP_COMMANDS_CONFIG_KEY = 'worktree.setup.commands' as ConfigKey;
|
|
24
|
+
export const WORKTREE_SETUP_CARRY_OVER_GLOBS_CONFIG_KEY = 'worktree.setup.carryOverGlobs' as ConfigKey;
|
|
25
|
+
|
|
26
|
+
const WORKTREE_SETUP_LIST_CONFIG_KEYS: ReadonlySet<ConfigKey> = new Set([
|
|
27
|
+
WORKTREE_SETUP_COMMANDS_CONFIG_KEY,
|
|
28
|
+
WORKTREE_SETUP_CARRY_OVER_GLOBS_CONFIG_KEY,
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** True for the two worktree-setup keys, whose stored value is a string array edited as a comma-separated list. */
|
|
32
|
+
export function isWorktreeSetupListConfigKey(key: ConfigKey): boolean {
|
|
33
|
+
return WORKTREE_SETUP_LIST_CONFIG_KEYS.has(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read a config value as a string array — mirrors the SDK's own tolerant
|
|
38
|
+
* readStringArray (non-array/malformed degrades to empty).
|
|
39
|
+
*
|
|
40
|
+
* Defensive try/catch (now belt-and-suspenders): the SDK registers the
|
|
41
|
+
* `worktree` section in DEFAULT_CONFIG (setup.commands / setup.carryOverGlobs,
|
|
42
|
+
* both empty lists) as of this repack, so ConfigManager.get no longer throws
|
|
43
|
+
* for these keys on a fresh store. The guard is retained so an older SDK — or
|
|
44
|
+
* a config store whose section was pruned — degrades to an empty list here
|
|
45
|
+
* (this read runs on every settings-modal build) rather than crashing /config
|
|
46
|
+
* or /settings.
|
|
47
|
+
*/
|
|
48
|
+
export function readWorktreeSetupList(configManager: Pick<ConfigManager, 'get'>, key: ConfigKey): string[] {
|
|
49
|
+
let raw: unknown;
|
|
50
|
+
try {
|
|
51
|
+
raw = configManager.get(key);
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(raw)) return [];
|
|
56
|
+
return raw.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Parse a comma-separated edit-buffer submission back into a string array (trimmed, empties dropped). */
|
|
60
|
+
export function parseWorktreeSetupListInput(text: string): string[] {
|
|
61
|
+
return text.split(',').map((part) => part.trim()).filter((part) => part.length > 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const WORKTREE_SETUP_COMMANDS_SYNTHETIC_SETTING: ConfigSetting = {
|
|
65
|
+
key: WORKTREE_SETUP_COMMANDS_CONFIG_KEY,
|
|
66
|
+
type: 'string',
|
|
67
|
+
default: [],
|
|
68
|
+
description: 'Comma-separated shell command lines run in order in a freshly-created isolated worktree before it is handed to an agent (e.g. "bun install, bun run codegen"). A command that exits non-zero stops setup there and marks the worktree failed — visible on /worktree review and the worktree record, never silent. Empty = no setup commands run.',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const WORKTREE_SETUP_CARRY_OVER_GLOBS_SYNTHETIC_SETTING: ConfigSetting = {
|
|
72
|
+
key: WORKTREE_SETUP_CARRY_OVER_GLOBS_CONFIG_KEY,
|
|
73
|
+
type: 'string',
|
|
74
|
+
default: [],
|
|
75
|
+
description: 'Comma-separated glob patterns (e.g. ".env, .env.*, config/local.json") of UNTRACKED files copied from the source working tree into a freshly-created isolated worktree, after setup commands run. Only files git reports as untracked are eligible — committed files already carry over through the worktree checkout itself. Empty = nothing carried over.',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function buildWorktreeSetupListEntry(configManager: Pick<ConfigManager, 'get'>, setting: ConfigSetting): SettingEntry {
|
|
79
|
+
const currentValue = readWorktreeSetupList(configManager, setting.key);
|
|
80
|
+
return {
|
|
81
|
+
setting,
|
|
82
|
+
currentValue,
|
|
83
|
+
isDefault: currentValue.length === 0,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildWorktreeSetupCommandsSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): SettingEntry {
|
|
88
|
+
return buildWorktreeSetupListEntry(configManager, WORKTREE_SETUP_COMMANDS_SYNTHETIC_SETTING);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildWorktreeSetupCarryOverGlobsSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): SettingEntry {
|
|
92
|
+
return buildWorktreeSetupListEntry(configManager, WORKTREE_SETUP_CARRY_OVER_GLOBS_SYNTHETIC_SETTING);
|
|
93
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -49,7 +49,7 @@ import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
|
49
49
|
import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
|
|
50
50
|
import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
|
|
51
51
|
import { installProcessLifecycle } from './runtime/process-lifecycle.ts';
|
|
52
|
-
import { createRenderScheduler } from '
|
|
52
|
+
import { createRenderScheduler } from '@pellux/goodvibes-terminal-shell';
|
|
53
53
|
import { buildCommandArgsHint } from './input/command-args-hint.ts';
|
|
54
54
|
import { summarizeRunningAgents } from './renderer/process-summary.ts';
|
|
55
55
|
import { footerFleetCost } from './panels/fleet-read-model.ts';
|
|
@@ -57,9 +57,15 @@ import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
|
57
57
|
import { wireStreamEventMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
|
|
58
58
|
import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
|
|
59
59
|
import { buildContextStatusHint } from './renderer/context-status-hint.ts';
|
|
60
|
+
import { isEffectiveDangerMode } from './config/index.ts';
|
|
61
|
+
import { createScriptableStatusline } from './core/scriptable-statusline.ts';
|
|
62
|
+
import { applyComposerCapture } from './input/composer-capture.ts';
|
|
63
|
+
import { createSessionAutoTitler } from './core/session-auto-titler.ts';
|
|
64
|
+
import { makeComposerEditorOpener } from './input/composer-editor.ts';
|
|
60
65
|
import { evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
61
66
|
import { createCancelGeneration } from './core/turn-cancellation.ts';
|
|
62
67
|
import { wrapRequestPermissionWithAlert } from './core/approval-alert.ts';
|
|
68
|
+
import { createTerminalNotifier } from './core/terminal-notifier.ts';
|
|
63
69
|
import { setPanelFrameRequester } from './panels/base-panel.ts';
|
|
64
70
|
|
|
65
71
|
import { ALT_SCREEN_ENTER, ALT_SCREEN_EXIT, MOUSE_ENABLE, MOUSE_DISABLE, CURSOR_HIDE, CURSOR_SHOW, CLEAR_SCREEN, KEYBOARD_EXT_ENABLE, KEYBOARD_EXT_DISABLE, PASTE_ENABLE, PASTE_DISABLE, FOCUS_ENABLE, FOCUS_DISABLE } from './renderer/terminal-escapes.ts';
|
|
@@ -223,17 +229,16 @@ async function main() {
|
|
|
223
229
|
setRecoveryInterval: (value) => { recoveryInterval = value; },
|
|
224
230
|
getStopSpokenOutputForExit: () => stopSpokenOutputForExit,
|
|
225
231
|
});
|
|
226
|
-
const {
|
|
227
|
-
exitApp,
|
|
228
|
-
resizeHandler,
|
|
229
|
-
sigintHandler,
|
|
230
|
-
unhandledRejectionHandler,
|
|
231
|
-
uncaughtExceptionHandler,
|
|
232
|
-
terminationSignalHandler,
|
|
233
|
-
exitListener,
|
|
234
|
-
} = lifecycle;
|
|
232
|
+
const { exitApp, resizeHandler, sigintHandler, unhandledRejectionHandler, uncaughtExceptionHandler, terminationSignalHandler, exitListener } = lifecycle;
|
|
235
233
|
commandContext.exit = exitApp;
|
|
236
234
|
|
|
235
|
+
// In-terminal (OSC 9) notifier (approval-wait/turn-end/agent-blocked); writes
|
|
236
|
+
// are restore-gated so no escape sequence lands after the shell resumes.
|
|
237
|
+
const terminalNotifier = createTerminalNotifier({
|
|
238
|
+
stdout, focusTracker: ctx.services.focusTracker, isReleased: () => lifecycle.isTerminalRestored(),
|
|
239
|
+
configGet: (k: string) => configManager.get(k as Parameters<typeof configManager.get>[0]),
|
|
240
|
+
});
|
|
241
|
+
|
|
237
242
|
const spokenTurns = wireSpokenTurnRuntime({
|
|
238
243
|
voiceService: ctx.services.voiceService,
|
|
239
244
|
configManager,
|
|
@@ -243,6 +248,15 @@ async function main() {
|
|
|
243
248
|
// Exit-path stop: bounded drain of the audio already playing (see stopForExit).
|
|
244
249
|
stopSpokenOutputForExit = () => spokenTurns.stopForExit();
|
|
245
250
|
unsubs.push(...spokenTurns.unsubs);
|
|
251
|
+
// Scriptable status line: runs the user's `statusline.command` at turn boundaries.
|
|
252
|
+
const scriptableStatusline = createScriptableStatusline({ configManager, cwd: workingDir, turns: uiServices.events.turns, onChange: () => render() });
|
|
253
|
+
unsubs.push(...scriptableStatusline.unsubs);
|
|
254
|
+
// Auto-title an untitled session via the weak/fast tool model (session.autoTitle, off by default).
|
|
255
|
+
const sessionAutoTitler = createSessionAutoTitler({
|
|
256
|
+
conversation, model: ctx.services.toolLLM, configManager, turns: uiServices.events.turns,
|
|
257
|
+
onTitled: (title) => { systemMessageRouter.high(`[Session] Auto-titled: "${title}"`); render(); },
|
|
258
|
+
});
|
|
259
|
+
unsubs.push(...sessionAutoTitler.unsubs);
|
|
246
260
|
unsubs.push(attachSpokenTurnModelRouting({
|
|
247
261
|
orchestrator,
|
|
248
262
|
providerRegistry,
|
|
@@ -269,18 +283,11 @@ async function main() {
|
|
|
269
283
|
}
|
|
270
284
|
processedText = processedText.replace(atModelMatch[0], '').trim();
|
|
271
285
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
processedText = '';
|
|
278
|
-
} else {
|
|
279
|
-
const memId = ctx.services.sessionMemoryStore.add(memoryText);
|
|
280
|
-
systemMessageRouter.high(`[Memory] Pinned: "${memoryText}" (${memId})`);
|
|
281
|
-
processedText = memoryText;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
286
|
+
// Composer capture markers: `!#` pins + sends; `#` saves a note without sending.
|
|
287
|
+
processedText = applyComposerCapture(processedText, {
|
|
288
|
+
sessionMemoryStore: ctx.services.sessionMemoryStore,
|
|
289
|
+
notify: (message) => systemMessageRouter.high(message),
|
|
290
|
+
}).text;
|
|
284
291
|
if (processedText || content) {
|
|
285
292
|
void (async () => {
|
|
286
293
|
const inputOptions = options.spokenOutput ? createSpokenTurnInputOptions() : undefined;
|
|
@@ -323,27 +330,27 @@ async function main() {
|
|
|
323
330
|
|
|
324
331
|
commandContext.submitInput = submitInput;
|
|
325
332
|
commandContext.submitSpokenInput = (text, content) => submitInput(text, content, { spokenOutput: true });
|
|
326
|
-
commandContext.stopSpokenOutput = () => spokenTurns.stop();
|
|
327
|
-
commandContext.pasteFromClipboard = () => input.handlePaste();
|
|
333
|
+
commandContext.stopSpokenOutput = () => spokenTurns.stop(); commandContext.pasteFromClipboard = () => input.handlePaste();
|
|
328
334
|
commandContext.executeCommand = (name, args) => commandRegistry.execute(name, args, commandContext);
|
|
335
|
+
// Late-patched: bootstrap.ts populates uiServices.platform.externalServices AFTER commandContext is built.
|
|
336
|
+
commandContext.platform.externalServices = uiServices.platform.externalServices;
|
|
329
337
|
commandContext.cancelGeneration = cancelGeneration;
|
|
330
338
|
commandContext.isGenerating = () => orchestrator.isThinking;
|
|
331
|
-
commandContext.jumpToBookmark = jumpToBookmark;
|
|
332
|
-
commandContext.scrollToLine = scrollToLine;
|
|
339
|
+
commandContext.jumpToBookmark = jumpToBookmark; commandContext.scrollToLine = scrollToLine;
|
|
333
340
|
commandContext.clearScreen = () => {
|
|
334
341
|
compositor.resetDiff();
|
|
335
342
|
allowTerminalWrite(() => stdout.write(CLEAR_SCREEN));
|
|
336
343
|
render();
|
|
337
344
|
};
|
|
338
|
-
commandContext.requestFullRepaint = () => { compositor.resetDiff(); render(); };
|
|
345
|
+
commandContext.requestFullRepaint = () => { compositor.resetDiff(); render(); }; commandContext.beginConcealedInput = (req) => input.beginConcealedInput(req);
|
|
339
346
|
permissionPromptRef.requestPermission = wrapRequestPermissionWithAlert((request) =>
|
|
340
347
|
new Promise((resolve) => {
|
|
341
348
|
pendingPermission = {
|
|
342
349
|
...request,
|
|
343
|
-
...buildPendingPermissionExtras(request, resolve),
|
|
350
|
+
...buildPendingPermissionExtras(request, resolve, approvalBroker),
|
|
344
351
|
};
|
|
345
352
|
render();
|
|
346
|
-
}), { focusTracker: ctx.services.focusTracker, configGet: (k: string) => configManager.get(k as Parameters<typeof configManager.get>[0]), webhookNotifier: ctx.services.webhookNotifier });
|
|
353
|
+
}), { focusTracker: ctx.services.focusTracker, configGet: (k: string) => configManager.get(k as Parameters<typeof configManager.get>[0]), webhookNotifier: ctx.services.webhookNotifier, terminalNotifier });
|
|
347
354
|
|
|
348
355
|
const input: InputHandler = new InputHandler(
|
|
349
356
|
() => render(),
|
|
@@ -403,6 +410,7 @@ async function main() {
|
|
|
403
410
|
orchestratorRefs.scrollToEnd = scrollToEnd;
|
|
404
411
|
|
|
405
412
|
input.setCommandRegistry(commandRegistry, commandContext);
|
|
413
|
+
commandContext.openComposerEditor = makeComposerEditorOpener({ buffer: input, stdin, stdout, writeGuard: allowTerminalWrite, repaint: () => { compositor.resetDiff(); render(); }, cwd: workingDir, env: process.env, notify: (m) => systemMessageRouter.high(m) });
|
|
406
414
|
input.setConversationManager(conversation);
|
|
407
415
|
input.setContentWidth(getPromptContentWidth());
|
|
408
416
|
input.filePicker.setOnUpdate(() => render());
|
|
@@ -481,21 +489,13 @@ async function main() {
|
|
|
481
489
|
provider: runtime.provider,
|
|
482
490
|
contextWindow,
|
|
483
491
|
contextStatusHint,
|
|
492
|
+
scriptableStatusLine: scriptableStatusline.current(),
|
|
484
493
|
// Compact footer posture on short terminals so the shell stays usable.
|
|
485
494
|
compact: height < 30,
|
|
486
495
|
// behavior.autoCompactThreshold is stored as a percent integer (e.g. 80);
|
|
487
496
|
// the meter expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
|
|
488
497
|
compactThreshold: Math.min(1, Math.max(0, (configManager.get('behavior.autoCompactThreshold') as number) / 100)),
|
|
489
|
-
dangerMode: (
|
|
490
|
-
if (configManager.get('behavior.autoApprove')) return true;
|
|
491
|
-
const permMode = configManager.get('permissions.mode');
|
|
492
|
-
if (permMode === 'allow-all') return true;
|
|
493
|
-
if (permMode === 'custom') {
|
|
494
|
-
const tools = configManager.getCategory('permissions').tools;
|
|
495
|
-
if (Object.values(tools).every(v => v === 'allow')) return true;
|
|
496
|
-
}
|
|
497
|
-
return false;
|
|
498
|
-
})(),
|
|
498
|
+
dangerMode: isEffectiveDangerMode(configManager),
|
|
499
499
|
lastInputTokens: orchestrator.lastInputTokens,
|
|
500
500
|
commandArgsHint,
|
|
501
501
|
hitlMode: modeManager.getHITLMode(),
|
|
@@ -508,7 +508,7 @@ async function main() {
|
|
|
508
508
|
composerMode: composerState.modeLabel,
|
|
509
509
|
composerStatus: composerState.statusLabel,
|
|
510
510
|
composerFlags: composerState.flags,
|
|
511
|
-
composerPendingRisk: composerState.pendingRisk,
|
|
511
|
+
composerPendingRisk: composerState.pendingRisk, permissionMode: configManager.get('permissions.mode') as string,
|
|
512
512
|
}).lines;
|
|
513
513
|
|
|
514
514
|
const onboardingOwnsScreen = input.onboardingWizard.active;
|
|
@@ -547,7 +547,7 @@ async function main() {
|
|
|
547
547
|
// Calculate how many rows are consumed by overlays (thinking, permissions, queue, file picker)
|
|
548
548
|
let overlayRows = 0;
|
|
549
549
|
if (orchestrator.isThinking) overlayRows += 2; // spinner + blank
|
|
550
|
-
if (pendingPermission) overlayRows += PermissionPromptUI.getPromptHeight(pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded);
|
|
550
|
+
if (pendingPermission) overlayRows += PermissionPromptUI.getPromptHeight(pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded, pendingPermission.requestedBy);
|
|
551
551
|
overlayRows += orchestrator.messageQueue.length * 3; // queued messages
|
|
552
552
|
// File picker and model picker overlay rows computed from actual rendered line count below
|
|
553
553
|
// Selection modal overlay rows are computed from actual rendered line count below
|
|
@@ -597,7 +597,7 @@ async function main() {
|
|
|
597
597
|
}
|
|
598
598
|
|
|
599
599
|
if (pendingPermission) {
|
|
600
|
-
viewport.push(...PermissionPromptUI.createPromptLines(conversationWidth, pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded));
|
|
600
|
+
viewport.push(...PermissionPromptUI.createPromptLines(conversationWidth, pendingPermission, pendingPermission.hunkState, pendingPermission.detailsExpanded, pendingPermission.requestedBy));
|
|
601
601
|
}
|
|
602
602
|
|
|
603
603
|
orchestrator.messageQueue.forEach(msg => {
|
|
@@ -687,7 +687,7 @@ async function main() {
|
|
|
687
687
|
gitStatusProvider,
|
|
688
688
|
lastGitInfoRef,
|
|
689
689
|
buildSessionContinuityHints,
|
|
690
|
-
render, webhookNotifier: ctx.services.webhookNotifier, focusTracker: ctx.services.focusTracker,
|
|
690
|
+
render, webhookNotifier: ctx.services.webhookNotifier, focusTracker: ctx.services.focusTracker, terminalNotifier, runtimeBus: uiServices.runtime.runtimeBus,
|
|
691
691
|
});
|
|
692
692
|
unsubs.push(...turnUnsubs);
|
|
693
693
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PanelManager } from '../panel-manager.ts';
|
|
2
2
|
import { GitPanel } from '../git-panel.ts';
|
|
3
3
|
import { DiffPanel } from '../diff-panel.ts';
|
|
4
|
+
import { DiffReviewPanel } from '../diff-review-panel.ts';
|
|
4
5
|
import { CostTrackerPanel } from '../cost-tracker-panel.ts';
|
|
5
6
|
import type { ResolvedBuiltinPanelDeps } from './shared.ts';
|
|
6
7
|
import { requireUiServices, withUnconfiguredFallback } from './shared.ts';
|
|
@@ -42,6 +43,19 @@ export function registerDevelopmentPanels(manager: PanelManager, deps: ResolvedB
|
|
|
42
43
|
factory: () => new DiffPanel(requireUiServices(deps).environment.workingDirectory, deps.requestRender),
|
|
43
44
|
});
|
|
44
45
|
|
|
46
|
+
manager.registerType({
|
|
47
|
+
id: 'review',
|
|
48
|
+
name: 'Review',
|
|
49
|
+
icon: 'R',
|
|
50
|
+
category: 'development',
|
|
51
|
+
description: 'Comment on session diff hunks and steer the comments to the model',
|
|
52
|
+
factory: () => new DiffReviewPanel(
|
|
53
|
+
requireUiServices(deps).environment.workingDirectory,
|
|
54
|
+
deps.requestRender,
|
|
55
|
+
deps.sessionChangeTracker ? () => deps.sessionChangeTracker!.getChangedFiles() : undefined,
|
|
56
|
+
),
|
|
57
|
+
});
|
|
58
|
+
|
|
45
59
|
// WO-110: 'inspector' registration moved to builtin/agent.ts (category
|
|
46
60
|
// 'agent') — it now absorbs the merged agent-logs capabilities.
|
|
47
61
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Diff review model — pure parsing + steering-payload helpers for the
|
|
3
|
+
// comment-on-hunk review loop (see diff-review-panel.ts).
|
|
4
|
+
//
|
|
5
|
+
// The diff this operates on is the git working tree vs a base ref (HEAD),
|
|
6
|
+
// filtered to the files the current session actually edited (the SDK's
|
|
7
|
+
// SessionChangeTracker). That is the honest source: it is CURRENT working-tree
|
|
8
|
+
// content, not a turn-start snapshot — the panel labels it exactly that way.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/** One hunk of one file's diff, with enough structure to steer a comment at it. */
|
|
12
|
+
export interface ReviewHunk {
|
|
13
|
+
readonly filePath: string;
|
|
14
|
+
readonly fileIndex: number;
|
|
15
|
+
readonly hunkIndex: number;
|
|
16
|
+
/** The raw `@@ -a,b +c,d @@` header line. */
|
|
17
|
+
readonly header: string;
|
|
18
|
+
readonly oldStart: number;
|
|
19
|
+
readonly oldCount: number;
|
|
20
|
+
readonly newStart: number;
|
|
21
|
+
readonly newCount: number;
|
|
22
|
+
/** Raw hunk body lines, each still carrying its leading ' ', '+' or '-'. */
|
|
23
|
+
readonly bodyLines: readonly string[];
|
|
24
|
+
readonly added: number;
|
|
25
|
+
readonly removed: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ReviewFile {
|
|
29
|
+
readonly filePath: string;
|
|
30
|
+
readonly fileIndex: number;
|
|
31
|
+
readonly hunks: readonly ReviewHunk[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Extract the target file path from one `diff --git`-delimited chunk. */
|
|
35
|
+
function extractFilePath(chunk: string): string {
|
|
36
|
+
const quotedGit = chunk.match(/^diff --git "a\/(.+)" "b\/(.+)"$/m);
|
|
37
|
+
if (quotedGit) return quotedGit[2]!;
|
|
38
|
+
const plainGit = chunk.match(/^diff --git a\/.+? b\/(.+)$/m);
|
|
39
|
+
if (plainGit) return plainGit[1]!;
|
|
40
|
+
const combined = chunk.match(/^diff --(?:cc|combined) "?([^"\n]+?)"?$/m);
|
|
41
|
+
if (combined) return combined[1]!;
|
|
42
|
+
const plus = chunk.match(/^\+\+\+ (?:b\/)?"?([^"\n]+?)"?\s*$/m);
|
|
43
|
+
if (plus && plus[1] !== '/dev/null') return plus[1]!;
|
|
44
|
+
const minus = chunk.match(/^--- (?:a\/)?"?([^"\n]+?)"?\s*$/m);
|
|
45
|
+
if (minus && minus[1] !== '/dev/null') return minus[1]!;
|
|
46
|
+
return 'unknown';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const HUNK_HEADER_RE = /^@@\s+-([0-9]+)(?:,([0-9]+))?\s+\+([0-9]+)(?:,([0-9]+))?\s+@@/;
|
|
50
|
+
|
|
51
|
+
/** Parse a multi-file unified `git diff` into per-file, per-hunk structures. */
|
|
52
|
+
export function parseReviewDiff(raw: string): ReviewFile[] {
|
|
53
|
+
const files: ReviewFile[] = [];
|
|
54
|
+
const chunks = raw.split(/(?=^diff --git |^diff --cc |^diff --combined )/m);
|
|
55
|
+
let fileIndex = 0;
|
|
56
|
+
for (const chunk of chunks) {
|
|
57
|
+
if (!chunk.trim()) continue;
|
|
58
|
+
const filePath = extractFilePath(chunk);
|
|
59
|
+
const hunks: ReviewHunk[] = [];
|
|
60
|
+
const lines = chunk.split('\n');
|
|
61
|
+
let current: { header: string; oldStart: number; oldCount: number; newStart: number; newCount: number; body: string[] } | null = null;
|
|
62
|
+
const flush = (): void => {
|
|
63
|
+
if (!current) return;
|
|
64
|
+
let added = 0;
|
|
65
|
+
let removed = 0;
|
|
66
|
+
for (const l of current.body) {
|
|
67
|
+
if (l.startsWith('+')) added++;
|
|
68
|
+
else if (l.startsWith('-')) removed++;
|
|
69
|
+
}
|
|
70
|
+
hunks.push({
|
|
71
|
+
filePath,
|
|
72
|
+
fileIndex,
|
|
73
|
+
hunkIndex: hunks.length,
|
|
74
|
+
header: current.header,
|
|
75
|
+
oldStart: current.oldStart,
|
|
76
|
+
oldCount: current.oldCount,
|
|
77
|
+
newStart: current.newStart,
|
|
78
|
+
newCount: current.newCount,
|
|
79
|
+
bodyLines: current.body,
|
|
80
|
+
added,
|
|
81
|
+
removed,
|
|
82
|
+
});
|
|
83
|
+
current = null;
|
|
84
|
+
};
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const m = line.match(HUNK_HEADER_RE);
|
|
87
|
+
if (m) {
|
|
88
|
+
flush();
|
|
89
|
+
current = {
|
|
90
|
+
header: line,
|
|
91
|
+
oldStart: parseInt(m[1]!, 10),
|
|
92
|
+
oldCount: m[2] !== undefined ? parseInt(m[2], 10) : 1,
|
|
93
|
+
newStart: parseInt(m[3]!, 10),
|
|
94
|
+
newCount: m[4] !== undefined ? parseInt(m[4], 10) : 1,
|
|
95
|
+
body: [],
|
|
96
|
+
};
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (current) current.body.push(line);
|
|
100
|
+
}
|
|
101
|
+
flush();
|
|
102
|
+
if (hunks.length > 0) files.push({ filePath, fileIndex, hunks });
|
|
103
|
+
fileIndex++;
|
|
104
|
+
}
|
|
105
|
+
return files;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Flatten files into a single navigable hunk list, in file/hunk order. */
|
|
109
|
+
export function flattenHunks(files: readonly ReviewFile[]): ReviewHunk[] {
|
|
110
|
+
const out: ReviewHunk[] = [];
|
|
111
|
+
for (const file of files) out.push(...file.hunks);
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The new-file line range a hunk covers (1-based, inclusive). */
|
|
116
|
+
export function hunkLineRange(hunk: ReviewHunk): { start: number; end: number } {
|
|
117
|
+
const start = hunk.newStart;
|
|
118
|
+
const end = hunk.newCount > 0 ? hunk.newStart + hunk.newCount - 1 : hunk.newStart;
|
|
119
|
+
return { start, end };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A short excerpt of a hunk's patch, header included, capped to `maxLines` body lines. */
|
|
123
|
+
export function hunkExcerpt(hunk: ReviewHunk, maxLines = 8): string {
|
|
124
|
+
const body = hunk.bodyLines.slice(0, maxLines);
|
|
125
|
+
const truncated = hunk.bodyLines.length > maxLines ? [`… (+${hunk.bodyLines.length - maxLines} more lines)`] : [];
|
|
126
|
+
return [hunk.header, ...body, ...truncated].join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The full unified-diff hunk text (header + body) for one ReviewHunk — the input checkpoints.revertHunk takes. */
|
|
130
|
+
export function hunkPatchText(hunk: ReviewHunk): string {
|
|
131
|
+
return [hunk.header, ...hunk.bodyLines].join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The subset of checkpoints.revertHunk's receipt the transcript block renders. */
|
|
135
|
+
export interface HunkRevertReceiptInput {
|
|
136
|
+
readonly path: string;
|
|
137
|
+
readonly hunkHeader: string;
|
|
138
|
+
readonly addedLinesRemoved: number;
|
|
139
|
+
readonly removedLinesRestored: number;
|
|
140
|
+
readonly safetyCheckpointId: string | null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build the distinct `[Revert]` transcript block for one applied hunk revert.
|
|
145
|
+
* Pure. The `[Revert]` prefix is force-surfaced inline (system-message-router.ts)
|
|
146
|
+
* so a working-tree mutation from /review is never a silent change.
|
|
147
|
+
*/
|
|
148
|
+
export function buildHunkRevertReceiptBlock(receipt: HunkRevertReceiptInput): string {
|
|
149
|
+
const plural = (n: number, word: string): string => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
150
|
+
const lines = [
|
|
151
|
+
`[Revert] Receipt — reverted one hunk in ${receipt.path} (${receipt.hunkHeader}).`,
|
|
152
|
+
` Restored ${plural(receipt.removedLinesRestored, 'deleted line')}, removed ${plural(receipt.addedLinesRemoved, 'added line')}.`,
|
|
153
|
+
receipt.safetyCheckpointId
|
|
154
|
+
? ` Reversible: a pre-revert checkpoint was taken — /checkpoints restore ${receipt.safetyCheckpointId} undoes it.`
|
|
155
|
+
: ' Reversible: the working tree already matched the latest checkpoint (nothing extra to snapshot).',
|
|
156
|
+
];
|
|
157
|
+
return lines.join('\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A comment attached to a specific hunk, ready to steer. */
|
|
161
|
+
export interface HunkComment {
|
|
162
|
+
readonly hunk: ReviewHunk;
|
|
163
|
+
readonly comment: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build the structured steering message a review comment submits to the session.
|
|
168
|
+
* Each block names the file, the new-file line range, the comment, and a short
|
|
169
|
+
* patch excerpt so the model knows exactly what the comment targets. The
|
|
170
|
+
* `sourceLabel` states the honest provenance of the diff (working tree vs base).
|
|
171
|
+
*/
|
|
172
|
+
export function buildSteerMessage(items: readonly HunkComment[], sourceLabel: string): string {
|
|
173
|
+
const blocks = items.map(({ hunk, comment }) => {
|
|
174
|
+
const range = hunkLineRange(hunk);
|
|
175
|
+
return [
|
|
176
|
+
`Review comment on ${hunk.filePath} (lines ${range.start}-${range.end}, new file):`,
|
|
177
|
+
comment,
|
|
178
|
+
'',
|
|
179
|
+
'Referenced change:',
|
|
180
|
+
'```diff',
|
|
181
|
+
hunkExcerpt(hunk),
|
|
182
|
+
'```',
|
|
183
|
+
].join('\n');
|
|
184
|
+
});
|
|
185
|
+
const header = items.length === 1
|
|
186
|
+
? `I am reviewing the current changes (${sourceLabel}) and left a comment on a specific hunk. Please address it.`
|
|
187
|
+
: `I am reviewing the current changes (${sourceLabel}) and left comments on ${items.length} hunks. Please address each one.`;
|
|
188
|
+
return [header, '', ...blocks].join('\n\n');
|
|
189
|
+
}
|