@pellux/goodvibes-tui 1.16.0 → 1.17.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 +95 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1091 -271
- 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/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/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +4 -4
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/stream-event-wiring.ts +8 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-cancellation.ts +1 -1
- package/src/core/turn-event-wiring.ts +8 -8
- package/src/daemon/cli.ts +20 -5
- 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 +34 -9
- 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/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/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/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- 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-runtime.ts +2 -2
- package/src/input/commands.ts +0 -2
- package/src/input/config-modal-types.ts +3 -3
- package/src/input/config-modal.ts +10 -11
- 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 +6 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +2 -2
- 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 +22 -21
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +23 -16
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +48 -48
- 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 +29 -10
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +4 -4
- package/src/panels/builtin-modals.ts +10 -10
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +5 -5
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-panel-format.ts +44 -8
- package/src/panels/fleet-panel.ts +16 -16
- package/src/panels/fleet-read-model.ts +52 -13
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +3 -3
- package/src/panels/fleet-tabs.ts +13 -14
- 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/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 +4 -4
- 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 +2 -2
- 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 +1 -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/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/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/onboarding/onboarding-wizard.ts +22 -8
- 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 +102 -24
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +14 -5
- package/src/renderer/settings-modal.ts +127 -39
- package/src/renderer/shell-surface.ts +3 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +1 -1
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +6 -1
- package/src/runtime/bootstrap-command-context.ts +4 -1
- package/src/runtime/bootstrap-command-parts.ts +6 -4
- package/src/runtime/bootstrap-core.ts +35 -35
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +6 -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 +55 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/index.ts +4 -21
- 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/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 +81 -82
- 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 +3 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +102 -6
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Line } from '../types/grid.ts';
|
|
2
2
|
import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
|
-
import type { SelectionModal } from '../input/selection-modal.ts';
|
|
3
|
+
import type { SelectionItem, SelectionModal } from '../input/selection-modal.ts';
|
|
4
4
|
import {
|
|
5
5
|
createOverlayBoxLayout,
|
|
6
6
|
createOverlayContentLine,
|
|
@@ -30,6 +30,45 @@ function putText(line: Line, startX: number, maxWidth: number, text: string, sty
|
|
|
30
30
|
putOverlayText(line, startX, maxWidth, text, style);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* How an item's `detail` text fits on screen. Descriptive text is never
|
|
35
|
+
* clipped: if it doesn't fit beside the label at a readable width, the full
|
|
36
|
+
* text wraps onto its own line(s) below the label instead of being
|
|
37
|
+
* truncated. `wrappedDetail` is null when the detail fits inline (no data
|
|
38
|
+
* lost either way); otherwise it holds every wrapped line the FULL detail
|
|
39
|
+
* needs (no line-count cap).
|
|
40
|
+
*/
|
|
41
|
+
interface DetailLayout {
|
|
42
|
+
readonly labelWidth: number;
|
|
43
|
+
readonly detailWidth: number;
|
|
44
|
+
readonly wrappedDetail: readonly string[] | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function computeDetailLayout(item: SelectionItem, remaining: number): DetailLayout {
|
|
48
|
+
if (!item.detail) {
|
|
49
|
+
return { labelWidth: remaining, detailWidth: 0, wrappedDetail: null };
|
|
50
|
+
}
|
|
51
|
+
const cols = fitLabelDetailColumns(item.label, item.detail, remaining);
|
|
52
|
+
const fitsInline = cols.detailWidth >= 12 && getDisplayWidth(item.detail) <= cols.detailWidth;
|
|
53
|
+
if (fitsInline) {
|
|
54
|
+
return { labelWidth: cols.labelWidth, detailWidth: cols.detailWidth, wrappedDetail: null };
|
|
55
|
+
}
|
|
56
|
+
// Nothing shares this row with the label once the detail moves below it,
|
|
57
|
+
// so the label itself gets the full row width too.
|
|
58
|
+
return {
|
|
59
|
+
labelWidth: remaining,
|
|
60
|
+
detailWidth: cols.detailWidth,
|
|
61
|
+
wrappedDetail: wrapWithHangingIndent(item.detail, Math.max(8, remaining), ''),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Physical rows an item occupies: 1 for the label/inline-detail row, plus
|
|
66
|
+
* one more per wrapped detail line when the detail didn't fit inline. */
|
|
67
|
+
function rowCostFor(item: SelectionItem, remaining: number): number {
|
|
68
|
+
if (!item.detail) return 1;
|
|
69
|
+
return 1 + (computeDetailLayout(item, remaining).wrappedDetail?.length ?? 0);
|
|
70
|
+
}
|
|
71
|
+
|
|
33
72
|
/**
|
|
34
73
|
* Render the selection modal as Line[] for overlay in the viewport.
|
|
35
74
|
*/
|
|
@@ -39,12 +78,17 @@ export function renderSelectionModalOverlay(
|
|
|
39
78
|
viewportHeight = 24,
|
|
40
79
|
): Line[] {
|
|
41
80
|
const lines: Line[] = [];
|
|
81
|
+
const chromeRows = modal.allowSearch ? 5 : 4;
|
|
42
82
|
const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
|
|
43
83
|
margin: 4,
|
|
44
84
|
maxWidth: 72,
|
|
45
|
-
chromeRows
|
|
85
|
+
chromeRows,
|
|
46
86
|
minContentRows: 6,
|
|
47
|
-
|
|
87
|
+
// Modals size to content: items whose detail wraps onto extra lines can
|
|
88
|
+
// need more physical rows than a plain one-row-per-item modal, so this
|
|
89
|
+
// ceiling grows with the viewport rather than staying pinned at a small
|
|
90
|
+
// constant (still bounded — never larger than the terminal can show).
|
|
91
|
+
maxContentRows: Math.max(10, viewportHeight - chromeRows - 4),
|
|
48
92
|
});
|
|
49
93
|
const layout = createOverlayBoxLayout(width, metrics.margin, metrics.boxWidth);
|
|
50
94
|
|
|
@@ -99,15 +143,51 @@ export function renderSelectionModalOverlay(
|
|
|
99
143
|
putText(line, layout.margin + 2, layout.innerWidth, fitDisplay(message, layout.innerWidth), { fg: MUTED_FG, dim: true });
|
|
100
144
|
lines.push(line);
|
|
101
145
|
} else {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
146
|
+
const indicatorWidth = 2;
|
|
147
|
+
const remaining = layout.innerWidth - indicatorWidth;
|
|
148
|
+
const rowBudget = Math.max(1, metrics.contentRows);
|
|
149
|
+
|
|
150
|
+
// Row-budget-aware windowing: rather than assuming one physical row per
|
|
151
|
+
// item, account for items whose detail wraps onto extra lines, so the
|
|
152
|
+
// visible window never silently shows fewer full items than the budget
|
|
153
|
+
// allows just because a fixed item-count cap ran out. Always shows the
|
|
154
|
+
// selected item (even alone, if its wrapped detail is taller than the
|
|
155
|
+
// whole budget — descriptive text is never clipped to make it fit),
|
|
156
|
+
// then grows outward, roughly centered, until the budget is spent.
|
|
157
|
+
const selected = Math.max(0, Math.min(modal.selectedIndex, items.length - 1));
|
|
158
|
+
let startIdx = selected;
|
|
159
|
+
let endIdx = selected + 1;
|
|
160
|
+
let usedRows = rowCostFor(items[selected], remaining);
|
|
161
|
+
let growBefore = true;
|
|
162
|
+
while (startIdx > 0 || endIdx < items.length) {
|
|
163
|
+
const canGrowBefore = growBefore && startIdx > 0;
|
|
164
|
+
const canGrowAfter = !growBefore && endIdx < items.length;
|
|
165
|
+
if (canGrowBefore) {
|
|
166
|
+
const cost = rowCostFor(items[startIdx - 1], remaining);
|
|
167
|
+
if (usedRows + cost > rowBudget) break;
|
|
168
|
+
startIdx -= 1;
|
|
169
|
+
usedRows += cost;
|
|
170
|
+
} else if (canGrowAfter) {
|
|
171
|
+
const cost = rowCostFor(items[endIdx], remaining);
|
|
172
|
+
if (usedRows + cost > rowBudget) break;
|
|
173
|
+
endIdx += 1;
|
|
174
|
+
usedRows += cost;
|
|
175
|
+
} else if (startIdx > 0) {
|
|
176
|
+
const cost = rowCostFor(items[startIdx - 1], remaining);
|
|
177
|
+
if (usedRows + cost > rowBudget) break;
|
|
178
|
+
startIdx -= 1;
|
|
179
|
+
usedRows += cost;
|
|
180
|
+
} else if (endIdx < items.length) {
|
|
181
|
+
const cost = rowCostFor(items[endIdx], remaining);
|
|
182
|
+
if (usedRows + cost > rowBudget) break;
|
|
183
|
+
endIdx += 1;
|
|
184
|
+
usedRows += cost;
|
|
185
|
+
} else {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
growBefore = !growBefore;
|
|
109
189
|
}
|
|
110
|
-
|
|
190
|
+
|
|
111
191
|
let lastCategory: string | undefined;
|
|
112
192
|
|
|
113
193
|
for (let i = startIdx; i < endIdx; i++) {
|
|
@@ -125,13 +205,10 @@ export function renderSelectionModalOverlay(
|
|
|
125
205
|
}
|
|
126
206
|
|
|
127
207
|
const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
|
|
128
|
-
const indicatorWidth = 2;
|
|
129
|
-
const remaining = layout.innerWidth - indicatorWidth;
|
|
130
208
|
const labelColor = isSelected ? TITLE_FG : (item.fg ?? BODY_FG);
|
|
131
209
|
const detailColor = isSelected ? BODY_FG : MUTED_FG;
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
: remaining;
|
|
210
|
+
const detailLayout = computeDetailLayout(item, remaining);
|
|
211
|
+
const labelWidth = detailLayout.labelWidth;
|
|
135
212
|
const labelLine = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
136
213
|
putText(labelLine, layout.margin + 2, indicatorWidth, indicator, {
|
|
137
214
|
fg: isSelected ? TITLE_FG : MUTED_FG,
|
|
@@ -144,23 +221,24 @@ export function renderSelectionModalOverlay(
|
|
|
144
221
|
bold: isSelected,
|
|
145
222
|
});
|
|
146
223
|
if (item.detail) {
|
|
147
|
-
|
|
148
|
-
|
|
224
|
+
if (detailLayout.wrappedDetail === null) {
|
|
225
|
+
// Fits beside the label at a readable width — no truncation needed.
|
|
149
226
|
putText(labelLine, layout.margin + 2 + indicatorWidth + labelWidth, 2, ' ', {
|
|
150
227
|
fg: BODY_FG,
|
|
151
228
|
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
152
229
|
});
|
|
153
|
-
putText(labelLine, layout.margin + 2 + indicatorWidth + labelWidth + 2, detailWidth, fitDisplay(
|
|
230
|
+
putText(labelLine, layout.margin + 2 + indicatorWidth + labelWidth + 2, detailLayout.detailWidth, fitDisplay(item.detail, detailLayout.detailWidth), {
|
|
154
231
|
fg: detailColor,
|
|
155
232
|
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
156
233
|
});
|
|
157
234
|
lines.push(labelLine);
|
|
158
235
|
} else {
|
|
236
|
+
// Doesn't fit beside the label — wrap the FULL detail text onto as
|
|
237
|
+
// many lines as it needs below the label, rather than clipping it.
|
|
159
238
|
lines.push(labelLine);
|
|
160
|
-
const
|
|
161
|
-
for (const detailLineText of wrappedDetails) {
|
|
239
|
+
for (const detailLineText of detailLayout.wrappedDetail) {
|
|
162
240
|
const detailLine = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
163
|
-
putText(detailLine, layout.margin + 2 + indicatorWidth, remaining, fitDisplay(
|
|
241
|
+
putText(detailLine, layout.margin + 2 + indicatorWidth, remaining, fitDisplay(detailLineText, remaining), {
|
|
164
242
|
fg: detailColor,
|
|
165
243
|
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
166
244
|
dim: !isSelected,
|
|
@@ -173,7 +251,7 @@ export function renderSelectionModalOverlay(
|
|
|
173
251
|
}
|
|
174
252
|
}
|
|
175
253
|
|
|
176
|
-
if (items.length
|
|
254
|
+
if (startIdx > 0 || endIdx < items.length) {
|
|
177
255
|
const above = startIdx;
|
|
178
256
|
const below = items.length - endIdx;
|
|
179
257
|
const scrollHint = above > 0 && below > 0
|
|
@@ -189,7 +267,7 @@ export function renderSelectionModalOverlay(
|
|
|
189
267
|
|
|
190
268
|
const footerLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
191
269
|
const selectedItem = modal.getSelected();
|
|
192
|
-
//
|
|
270
|
+
// vocab unification: a caller-supplied primaryVerbLabel (e.g. /help's
|
|
193
271
|
// "Run", matching the slash-command palette) wins over the generic
|
|
194
272
|
// per-primaryAction default.
|
|
195
273
|
const primaryVerb = modal.primaryVerbLabel
|
|
@@ -42,7 +42,7 @@ function isClosedStatus(status: string): boolean {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* Closed sessions carry an optional, honest reason for WHY they
|
|
46
46
|
* closed under `metadata.closeReason` (SDK's `SharedSessionCloseReason`,
|
|
47
47
|
* 'closeReason' key — see `@pellux/goodvibes-sdk` platform/control-plane
|
|
48
48
|
* session-broker-sessions.ts's `readSessionCloseReason`). `metadata` is an
|
|
@@ -58,7 +58,7 @@ function readCloseReason(record: SharedSessionRecord): string | undefined {
|
|
|
58
58
|
/**
|
|
59
59
|
* A GC sweep closing an idle session ('idle-reaped') auto-reopens on the next
|
|
60
60
|
* heartbeat — it is NOT the same event as a deliberate user/surface close, so
|
|
61
|
-
* it must never render under the same "closed" badge
|
|
61
|
+
* it must never render under the same "closed" badge. Tolerant of
|
|
62
62
|
* records without the field (pre-feature builds, or a deliberate close).
|
|
63
63
|
*/
|
|
64
64
|
function isReapedRecord(record: SharedSessionRecord): boolean {
|
|
@@ -11,7 +11,7 @@ import { UI_TONES } from './ui-primitives.ts';
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* "Modified / active" accent for settings rows — cyan-green, no UI_TONES
|
|
14
|
-
* role matches it (
|
|
14
|
+
* role matches it (preserved byte-exact as a named constant).
|
|
15
15
|
*/
|
|
16
16
|
const SETTINGS_ACCENT = '#00ffcc';
|
|
17
17
|
|
|
@@ -31,6 +31,14 @@ export function formatValue(entry: SettingEntry): string {
|
|
|
31
31
|
// Array-backed settings (e.g. worktree.setup.commands) display as a
|
|
32
32
|
// comma-separated list, matching how they're edited (see worktree-setup-config.ts).
|
|
33
33
|
if (Array.isArray(val)) return val.length > 0 ? val.join(', ') : '(empty)';
|
|
34
|
+
// Object-backed settings (e.g. pricing.modelPrices) display their JSON when
|
|
35
|
+
// short, or an honest entry count — never the useless "[object Object]".
|
|
36
|
+
if (typeof val === 'object') {
|
|
37
|
+
const keys = Object.keys(val as Record<string, unknown>);
|
|
38
|
+
if (keys.length === 0) return '(none set)';
|
|
39
|
+
const json = JSON.stringify(val);
|
|
40
|
+
return json.length <= 64 ? json : `${keys.length} entr${keys.length === 1 ? 'y' : 'ies'} (Enter to edit as JSON)`;
|
|
41
|
+
}
|
|
34
42
|
return String(val);
|
|
35
43
|
}
|
|
36
44
|
|
|
@@ -57,6 +65,8 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
57
65
|
display: 'Display',
|
|
58
66
|
ui: 'UI',
|
|
59
67
|
provider: 'Provider',
|
|
68
|
+
pricing: 'Pricing',
|
|
69
|
+
update: 'Updates',
|
|
60
70
|
subscriptions: 'Subscriptions',
|
|
61
71
|
behavior: 'Behavior',
|
|
62
72
|
storage: 'Storage',
|
|
@@ -87,7 +97,6 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
87
97
|
release: 'Release',
|
|
88
98
|
danger: 'Danger',
|
|
89
99
|
tools: 'Tools',
|
|
90
|
-
flags: 'Advanced Features',
|
|
91
100
|
network: 'Network',
|
|
92
101
|
relay: 'Relay',
|
|
93
102
|
learning: 'Learning',
|
|
@@ -171,9 +180,9 @@ const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
|
171
180
|
};
|
|
172
181
|
|
|
173
182
|
export function getSettingLabel(entry: SettingEntry): string {
|
|
174
|
-
// Feature-unit
|
|
175
|
-
//
|
|
176
|
-
if (entry.flag) return entry.flag.
|
|
183
|
+
// Feature-unit headers carry the feature's human name, not the raw
|
|
184
|
+
// enablement-key tail.
|
|
185
|
+
if (entry.flag) return entry.flag.feature.name;
|
|
177
186
|
return SETTING_LABELS[entry.setting.key] ?? entry.setting.key.replace(/^[^.]+\./, '');
|
|
178
187
|
}
|
|
179
188
|
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Line } from '../types/grid.ts';
|
|
9
|
-
import type { SettingsModal, SettingEntry,
|
|
9
|
+
import type { SettingsModal, SettingEntry, McpEntry, SubscriptionEntry, SettingsCategory } from '../input/settings-modal.ts';
|
|
10
10
|
import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings-modal.ts';
|
|
11
|
+
import { FEATURE_SETTINGS_BY_ID, isFeatureValueEnabled } from '../runtime/feature-settings.ts';
|
|
11
12
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
12
13
|
import { CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
|
|
13
14
|
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
@@ -27,6 +28,8 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
27
28
|
display: 'Presentation settings for the terminal transcript: streaming, line numbers, thinking visibility, reasoning summaries, token speed, and tool previews.',
|
|
28
29
|
ui: 'Controls where operational messages render and whether voice surfaces are enabled. These settings change visibility, not provider behavior.',
|
|
29
30
|
provider: 'Default model routing for normal chat turns, embeddings, reasoning effort, and persistent system prompt file.',
|
|
31
|
+
pricing: 'Manual model prices (USD per 1M tokens, keyed provider:model). Your price wins over provider-served and catalog prices in every cost display, immediately.',
|
|
32
|
+
update: 'Self-update posture: whether the daemon checks hourly and updates at an idle moment, the release source it checks, and the check cadence. Clients update at launch; every swap keeps the previous binary for one-command rollback.',
|
|
30
33
|
subscriptions: 'Provider subscription login state and routing posture. Active sessions can be reviewed or signed out here; API keys remain managed through secrets.',
|
|
31
34
|
behavior: 'Day-to-day shell behavior: approval posture, compaction, history, guidance, notifications, stale-context warnings, return context, and Human-in-the-Loop mode.',
|
|
32
35
|
storage: 'Local storage posture, including secret storage policy and maximum artifact size for knowledge/home graph/document ingestion.',
|
|
@@ -57,7 +60,6 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
57
60
|
release: 'Release-channel preference.',
|
|
58
61
|
danger: 'High-impact switches for daemon and HTTP listener behavior. These are operational overrides, not normal preferences.',
|
|
59
62
|
tools: 'Tool LLM and helper model routing. Empty provider/model values inherit the active chat route unless a specific helper/tool route is set.',
|
|
60
|
-
flags: 'Advanced feature toggles with no tuning knobs — internal and startup-gated runtime gates. Features that DO have config sit as units (toggle + settings) in their topical category, not here.',
|
|
61
63
|
network: 'Combined network view for daemon control-plane, HTTP listener, browser web surface, and general outbound network settings.',
|
|
62
64
|
fetch: 'Fetch response sanitization and host trust tiers for the fetch tool: sanitize mode plus default trusted/blocked host lists. Gated by the Fetch Response Sanitization feature.',
|
|
63
65
|
agents: 'Sub-agent context-window awareness and per-turn passive knowledge/code injection: token budget, relevance floor, code-chunk limit, and the compaction threshold. Gated by the agent context/injection features.',
|
|
@@ -65,7 +67,7 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
65
67
|
integrations: 'Integration delivery reliability (Slack/Discord/webhook): retry counts, backoff bounds, dead-letter queue size, and SLO enforcement. Gated by the Integration Delivery SLO feature.',
|
|
66
68
|
policy: 'Policy-as-code bundle loading: where the startup policy bundle is loaded from and its file path. Gated by the Policy-as-Code feature.',
|
|
67
69
|
notifications: 'Notification router burst-suppression tuning: burst window, threshold, and cooldown. Gated by the Adaptive Notification Suppression feature.',
|
|
68
|
-
relay: 'Outbound zero-knowledge relay reachability, for reaching this daemon from outside the LAN without opening an inbound port. Off by default
|
|
70
|
+
relay: 'Outbound zero-knowledge relay reachability, for reaching this daemon from outside the LAN without opening an inbound port. Off by default — relay.enabled is the switch. The relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
|
|
69
71
|
learning: 'Idle-time memory consolidation: merging duplicate standing memory records and decaying/archiving stale ones. Off by default — nothing runs until enabled.',
|
|
70
72
|
};
|
|
71
73
|
|
|
@@ -181,6 +183,13 @@ function buildSettingContext(modal: SettingsModal, entry: SettingEntry): string[
|
|
|
181
183
|
if (entry.locked) lines.push(`Locked: ${entry.lockReason ?? 'This setting is locked by a higher-priority layer.'}`);
|
|
182
184
|
if (entry.conflict) lines.push(`Conflict: resolve with /settings-sync resolve ${entry.setting.key} local|synced.`);
|
|
183
185
|
|
|
186
|
+
// A settings sub-row owned by a feature unit names its feature so "what
|
|
187
|
+
// does this do" is answerable without scrolling back to the header row.
|
|
188
|
+
if (entry.ownerFlagId) {
|
|
189
|
+
const owner = FEATURE_SETTINGS_BY_ID.get(entry.ownerFlagId);
|
|
190
|
+
if (owner) lines.push(`Part of feature: ${owner.name} (the header row above).`);
|
|
191
|
+
}
|
|
192
|
+
|
|
184
193
|
lines.push('', entry.setting.description);
|
|
185
194
|
|
|
186
195
|
if (
|
|
@@ -225,26 +234,86 @@ function buildSettingContext(modal: SettingsModal, entry: SettingEntry): string[
|
|
|
225
234
|
return lines;
|
|
226
235
|
}
|
|
227
236
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
/**
|
|
238
|
+
* The option shape of a feature header, rendered from the same schema the
|
|
239
|
+
* write path uses: enum headers list every mode choice (marking the current
|
|
240
|
+
* value and which values keep the feature active), boolean headers state the
|
|
241
|
+
* two positions.
|
|
242
|
+
*/
|
|
243
|
+
function buildFeatureOptionLines(entry: SettingEntry): string[] {
|
|
244
|
+
const feature = entry.flag!.feature;
|
|
245
|
+
const setting = entry.setting;
|
|
246
|
+
const lines: string[] = [];
|
|
247
|
+
if (setting.type === 'enum' && setting.enumValues) {
|
|
248
|
+
lines.push('', `Mode choices for ${setting.key}:`);
|
|
249
|
+
const activeValues = feature.enablement.enabledValues ?? [];
|
|
250
|
+
const descriptions = ENUM_VALUE_DESCRIPTIONS[setting.key] ?? {};
|
|
251
|
+
for (const value of setting.enumValues) {
|
|
252
|
+
const marks: string[] = [];
|
|
253
|
+
if (value === String(entry.currentValue)) marks.push('current');
|
|
254
|
+
if (feature.enablement.kind === 'enum') {
|
|
255
|
+
marks.push(activeValues.includes(value) ? 'feature on' : 'feature off');
|
|
256
|
+
}
|
|
257
|
+
const suffix = marks.length > 0 ? ` (${marks.join(', ')})` : '';
|
|
258
|
+
lines.push(`${value}${suffix}: ${descriptions[value] ?? `Use ${value} for this setting.`}`);
|
|
259
|
+
}
|
|
260
|
+
} else if (setting.type === 'boolean') {
|
|
261
|
+
lines.push('');
|
|
262
|
+
lines.push('Possible values:');
|
|
263
|
+
lines.push('true: the feature is enabled.');
|
|
264
|
+
lines.push('false: the feature is disabled.');
|
|
265
|
+
}
|
|
266
|
+
return lines;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Under-cursor documentation for a feature-unit header, rendered entirely
|
|
271
|
+
* from the SDK's per-feature settings metadata: full behavior description,
|
|
272
|
+
* the real option shape, every settings key that tunes the feature, and the
|
|
273
|
+
* honest live/restart state from the gate manager.
|
|
274
|
+
*/
|
|
275
|
+
function buildFlagContext(entry: SettingEntry | null): string[] {
|
|
276
|
+
const flagEntry = entry?.flag ?? null;
|
|
277
|
+
if (!entry || !flagEntry) return ['Features', 'No feature is selected.'];
|
|
278
|
+
const { feature, flag, state, persistedState, pendingRestart } = flagEntry;
|
|
279
|
+
const configOn = isFeatureValueEnabled(feature, entry.currentValue);
|
|
280
|
+
const displayState = state === 'killed' ? 'killed' : configOn ? 'enabled' : 'disabled';
|
|
281
|
+
const lines: string[] = [
|
|
282
|
+
feature.name,
|
|
283
|
+
`Feature: ${feature.id} (${feature.domain} domain)`,
|
|
284
|
+
`Setting: ${feature.enablement.key} = ${formatValue(entry)}`,
|
|
285
|
+
`State: ${displayState}`,
|
|
286
|
+
`Default: ${feature.defaultEnabled ? 'enabled' : 'disabled'}`,
|
|
287
|
+
`Applies: ${feature.restartRequired ? 'on next launch (startup-gated)' : 'immediately'}`,
|
|
288
|
+
...(pendingRestart
|
|
289
|
+
? [`Pending restart: saved as ${persistedState}; effective state stays ${state} until the next launch.`]
|
|
239
290
|
: []),
|
|
240
291
|
'',
|
|
241
|
-
|
|
242
|
-
...(entry.state === 'killed' && entry.flag.killReason ? ['', `Kill reason: ${entry.flag.killReason}`] : []),
|
|
243
|
-
'',
|
|
244
|
-
entry.flag.runtimeToggleable
|
|
245
|
-
? 'Impact: changes apply immediately and are also persisted as an override when they differ from the default.'
|
|
246
|
-
: 'Impact: this flag is persisted as an override and requires restart before startup-only code sees the new state.',
|
|
292
|
+
feature.description,
|
|
247
293
|
];
|
|
294
|
+
|
|
295
|
+
lines.push(...buildFeatureOptionLines(entry));
|
|
296
|
+
|
|
297
|
+
lines.push('');
|
|
298
|
+
if (feature.enablement.kind === 'enum') {
|
|
299
|
+
lines.push(`How it turns on: active while ${feature.enablement.key} is ${(feature.enablement.enabledValues ?? []).join(' or ')}.`);
|
|
300
|
+
} else if (feature.enablement.kind === 'constant' && entry.setting.type !== 'boolean') {
|
|
301
|
+
lines.push('How it turns on: always active; the settings listed below tune its behavior directly.');
|
|
302
|
+
} else {
|
|
303
|
+
lines.push(`How it turns on: ${feature.enablement.key} set to true.`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (feature.settings.length > 1) {
|
|
307
|
+
lines.push('', 'Settings in this feature:');
|
|
308
|
+
for (const key of feature.settings) {
|
|
309
|
+
lines.push(key === feature.enablement.key ? `${key} (this row)` : key);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (state === 'killed' && flag.killReason) {
|
|
314
|
+
lines.push('', `Kill reason: ${flag.killReason}`);
|
|
315
|
+
}
|
|
316
|
+
return lines;
|
|
248
317
|
}
|
|
249
318
|
|
|
250
319
|
function buildMcpContext(modal: SettingsModal, entry: McpEntry | null): string[] {
|
|
@@ -332,9 +401,10 @@ function buildContextLines(modal: SettingsModal, width: number): string[] {
|
|
|
332
401
|
lines.push(...buildSubscriptionContext(modal, modal.getSelectedSubscription()));
|
|
333
402
|
} else {
|
|
334
403
|
const selected = modal.getSelected();
|
|
335
|
-
// A feature-unit
|
|
336
|
-
//
|
|
337
|
-
|
|
404
|
+
// A feature-unit header shows the feature's documentation (full
|
|
405
|
+
// description, option shape, settings list, live/restart state); its
|
|
406
|
+
// settings sub-rows and plain settings show the setting context.
|
|
407
|
+
if (selected?.flag) lines.push(...buildFlagContext(selected));
|
|
338
408
|
else if (selected) lines.push(...buildSettingContext(modal, selected));
|
|
339
409
|
else lines.push('No setting is selected in this category.');
|
|
340
410
|
}
|
|
@@ -355,7 +425,6 @@ function buildContextLines(modal: SettingsModal, width: number): string[] {
|
|
|
355
425
|
function categoryItemCount(modal: SettingsModal, category: SettingsCategory): number {
|
|
356
426
|
if (category === 'mcp') return modal.mcpEntries.length;
|
|
357
427
|
if (category === 'subscriptions') return modal.subscriptionEntries.length;
|
|
358
|
-
// 'flags' (Advanced Features) now flows through the normal group path.
|
|
359
428
|
return modal.groups.get(category)?.length ?? 0;
|
|
360
429
|
}
|
|
361
430
|
|
|
@@ -454,15 +523,18 @@ function renderSettingTableRow(
|
|
|
454
523
|
defaultWidth: number,
|
|
455
524
|
): string {
|
|
456
525
|
if (entry.flag) {
|
|
457
|
-
const {
|
|
458
|
-
const
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
//
|
|
463
|
-
// the
|
|
464
|
-
|
|
465
|
-
|
|
526
|
+
const { feature, state, pendingRestart } = entry.flag;
|
|
527
|
+
const configOn = isFeatureValueEnabled(feature, entry.currentValue);
|
|
528
|
+
const stateMark = state === 'killed' ? '✕' : configOn ? '●' : '○';
|
|
529
|
+
const label = `${stateMark} ${feature.name}`;
|
|
530
|
+
const source = feature.restartRequired ? 'restart' : 'live';
|
|
531
|
+
// A startup-gated feature changed this session shows its saved value with
|
|
532
|
+
// a compact restart-pending marker (⟳), so the row never implies the
|
|
533
|
+
// change already took effect; the documentation pane spells out the full
|
|
534
|
+
// "Pending restart: ..." sentence.
|
|
535
|
+
const rawValue = currentSettingValue(modal, entry, selected);
|
|
536
|
+
const value = pendingRestart ? `${rawValue} ⟳` : rawValue;
|
|
537
|
+
return `${marker} ${padDisplay(label, keyWidth)} ${padDisplay(value, valueWidth)} ${padDisplay('feature', typeWidth)} ${padDisplay(source, sourceWidth)} ${padDisplay(feature.defaultEnabled ? 'enabled' : 'disabled', defaultWidth)}`;
|
|
466
538
|
}
|
|
467
539
|
const value = currentSettingValue(modal, entry, selected);
|
|
468
540
|
const source = `${entry.effectiveSource ?? 'default'}${entry.locked ? ' locked' : ''}${entry.conflict ? ' conflict' : ''}`;
|
|
@@ -626,9 +698,9 @@ function footerText(modal: SettingsModal, width: number): string {
|
|
|
626
698
|
{ key: 'Enter', verb: 'Edit trust' },
|
|
627
699
|
{ key: 'Esc', verb: 'Close' },
|
|
628
700
|
]));
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
//
|
|
701
|
+
// Every topical category that hosts feature units flows through the default
|
|
702
|
+
// settings footer below — Enter/Space toggles a feature-unit header exactly
|
|
703
|
+
// as it edits/toggles any setting row.
|
|
632
704
|
// Default settings pane: tier the reset affordances by available width.
|
|
633
705
|
// W<80: minimal — only the most critical action survives.
|
|
634
706
|
// W<160: compact but still shows both reset affordances.
|
|
@@ -639,6 +711,7 @@ function footerText(modal: SettingsModal, width: number): string {
|
|
|
639
711
|
return formatHints([
|
|
640
712
|
{ key: 'Up/Down', verb: 'Move' },
|
|
641
713
|
{ key: 'Enter/Space', verb: 'Edit' },
|
|
714
|
+
{ key: 'PgUp/PgDn', verb: 'docs' },
|
|
642
715
|
{ key: '⇧R', verb: 'reset cat' },
|
|
643
716
|
{ key: '^⇧R', verb: 'reset all' },
|
|
644
717
|
{ key: 'Esc', verb: 'Close' },
|
|
@@ -647,6 +720,7 @@ function footerText(modal: SettingsModal, width: number): string {
|
|
|
647
720
|
{ key: 'Up/Down', verb: 'Setting' },
|
|
648
721
|
{ key: 'Left', verb: 'Categories' },
|
|
649
722
|
{ key: 'Enter/Space', verb: 'Edit/toggle' },
|
|
723
|
+
{ key: 'PgUp/PgDn', verb: 'Scroll docs' },
|
|
650
724
|
{ key: '⇧R', verb: 'reset cat' },
|
|
651
725
|
{ key: '^⇧R', verb: 'reset all' },
|
|
652
726
|
{ key: 'Esc', verb: 'Close' },
|
|
@@ -664,12 +738,26 @@ export function renderSettingsModal(
|
|
|
664
738
|
];
|
|
665
739
|
const metrics = getFullscreenWorkspaceMetrics({ width, height: viewportHeight });
|
|
666
740
|
const categoryRows = renderCategories(modal, metrics.leftWidth - 2, metrics.bodyRows);
|
|
667
|
-
|
|
741
|
+
// Documentation pane: long content SCROLLS (PgUp/PgDn) with honest
|
|
742
|
+
// more-above/below markers — it is never silently clipped.
|
|
743
|
+
const allContextLines = buildContextLines(modal, metrics.contextWidth);
|
|
744
|
+
const visibleContext = metrics.contextRows;
|
|
745
|
+
const maxContextScroll = Math.max(0, allContextLines.length - visibleContext);
|
|
746
|
+
const contextOffset = Math.min(Math.max(0, modal.contextScroll ?? 0), maxContextScroll);
|
|
747
|
+
const windowedContext = allContextLines.slice(contextOffset, contextOffset + visibleContext);
|
|
748
|
+
if (contextOffset > 0) {
|
|
749
|
+
windowedContext[0] = `${GLYPHS.navigation.moreAbove} ${contextOffset} more line(s) above — PgUp`;
|
|
750
|
+
}
|
|
751
|
+
if (contextOffset + visibleContext < allContextLines.length) {
|
|
752
|
+
const below = allContextLines.length - contextOffset - visibleContext;
|
|
753
|
+
windowedContext[windowedContext.length - 1] = `${GLYPHS.navigation.moreBelow} ${below} more line(s) below — PgDn`;
|
|
754
|
+
}
|
|
755
|
+
const contextRows = windowedContext.map((text, row): WorkspaceRow => {
|
|
668
756
|
const selectedSetting = modal.getSelected();
|
|
669
|
-
const isTitle = row === 0 || (selectedSetting !== null && text === getSettingLabel(selectedSetting));
|
|
757
|
+
const isTitle = (row === 0 && contextOffset === 0) || (selectedSetting !== null && text === getSettingLabel(selectedSetting));
|
|
670
758
|
return {
|
|
671
759
|
text,
|
|
672
|
-
fg: row === 0 ? PALETTE.title : text.endsWith(':') ? PALETTE.subtitle : PALETTE.text,
|
|
760
|
+
fg: row === 0 && contextOffset === 0 ? PALETTE.title : text.endsWith(':') ? PALETTE.subtitle : PALETTE.text,
|
|
673
761
|
bold: isTitle,
|
|
674
762
|
dim: text.length === 0,
|
|
675
763
|
};
|
|
@@ -65,6 +65,8 @@ export interface ShellFooterBuildOptions {
|
|
|
65
65
|
* embedded/local mode so no segment renders.
|
|
66
66
|
*/
|
|
67
67
|
readonly sessionSpineStatus?: 'online' | 'offline';
|
|
68
|
+
/** The web surface's reachable URL, when that surface is enabled; a persistent footer segment. */
|
|
69
|
+
readonly webSurfaceUrl?: string;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export interface ShellFooterBuildResult {
|
|
@@ -144,6 +146,7 @@ export function buildShellFooter(
|
|
|
144
146
|
options.compact ?? false,
|
|
145
147
|
options.sessionSpineStatus,
|
|
146
148
|
options.permissionMode,
|
|
149
|
+
options.webSurfaceUrl,
|
|
147
150
|
);
|
|
148
151
|
// Compact posture drops the process indicator and context hint entirely so
|
|
149
152
|
// the footer fits within ~5 rows on short terminals.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* terminal-bg-probe — OSC 11 terminal-background detection for `auto` theme mode.
|
|
3
3
|
*
|
|
4
|
-
|
|
4
|
+
*. On startup, when appearance is `auto` and stdout is a TTY, we ask
|
|
5
5
|
* the terminal for its background colour with an OSC 11 query and classify the
|
|
6
6
|
* reply as light or dark. Everything about this is best-effort and conservative:
|
|
7
7
|
* any timeout, any unparseable reply, any ambiguity resolves to DARK, which is
|
package/src/renderer/theme.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* DEFAULT_PANEL_PALETTE, DEFAULT_STYLE, FULLSCREEN_PALETTE and
|
|
22
22
|
* DEFAULT_OVERLAY_PALETTE all read through ONE mode-resolved path instead of
|
|
23
23
|
* importing the static UI_TONES constant directly. Mode stays 'dark'
|
|
24
|
-
* everywhere until the terminal-bg-probe lands (see
|
|
24
|
+
* everywhere until the terminal-bg-probe lands (see) — the light
|
|
25
25
|
* entry is type-complete, not a shipped light theme.
|
|
26
26
|
*/
|
|
27
27
|
|
|
@@ -173,7 +173,7 @@ export const DARK_THEME: Readonly<ThemeTokens> = DARK;
|
|
|
173
173
|
//
|
|
174
174
|
// UI_TONES (ui-primitives.ts) is the dark entry. The light entry mirrors
|
|
175
175
|
// dark for every role that has no light-appropriate equivalent yet — the
|
|
176
|
-
//
|
|
176
|
+
// deliverable is the mode dimension and single read path, not a
|
|
177
177
|
// shipped light chrome theme (see module doc comment above).
|
|
178
178
|
// ---------------------------------------------------------------------------
|
|
179
179
|
|
|
@@ -242,7 +242,7 @@ export function resolveUiTones(mode: ThemeMode): Readonly<UiToneTokens> {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
// ===========================================================================
|
|
245
|
-
// Active-mode runtime (
|
|
245
|
+
// Active-mode runtime (terminal-bg-probe landing).
|
|
246
246
|
//
|
|
247
247
|
// The mode is decided ONCE at startup — from appearance config (display.themeMode
|
|
248
248
|
// forced dark/light) or the terminal-background probe (auto) — and is then stable
|
|
@@ -173,7 +173,7 @@ export function renderToolCallBlock(
|
|
|
173
173
|
|
|
174
174
|
// Status icon. 'pending' means the tool is still awaiting a decision
|
|
175
175
|
// (e.g. an approval prompt) and has NOT run yet — it uses the hollow idle
|
|
176
|
-
// glyph so a not-yet-run tool never shows the completed green ✓ (
|
|
176
|
+
// glyph so a not-yet-run tool never shows the completed green ✓ (2c).
|
|
177
177
|
const icon = status === 'done' ? TOOL_STATUS.SUCCESS_ICON
|
|
178
178
|
: status === 'error' ? TOOL_STATUS.FAIL_ICON
|
|
179
179
|
: status === 'pending' ? GLYPHS.status.idle
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* in conversation-rendering.ts) shows "wrote haiku.txt (532 B)" instead of a
|
|
5
5
|
* raw `{"files_written":1,"bytes_written":532,...}` JSON blob. The full payload
|
|
6
6
|
* stays reachable behind the existing collapse/expand toggle — this only
|
|
7
|
-
* changes the collapsed preview line, never destroys data. (
|
|
7
|
+
* changes the collapsed preview line, never destroys data. (item 3.)
|
|
8
8
|
*
|
|
9
9
|
* Returning `null` means "no honest summary for this shape" — the caller then
|
|
10
10
|
* falls back to the previous raw-first-line preview, so an unrecognised tool or
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-turn knowledge injection record rendering.
|
|
3
3
|
*
|
|
4
|
-
* The SDK's passive per-turn retrieval engine
|
|
5
|
-
* packages/sdk/src/platform/agents/turn-knowledge-injection.ts) stores one
|
|
4
|
+
* The SDK's passive per-turn retrieval engine
|
|
5
|
+
* (packages/sdk/src/platform/agents/turn-knowledge-injection.ts) stores one
|
|
6
6
|
* `TurnInjectionRecord` per turn on `AgentRecord.turnInjections` (a bounded
|
|
7
7
|
* ring, default 20 entries) and appends the same record to the agent's
|
|
8
8
|
* session transcript as `{type:'knowledge_injection', turn, ...record}`.
|
|
@@ -34,10 +34,11 @@ function fmtN(n: number): string {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Annotate each injected id with its source
|
|
37
|
+
* Annotate each injected id with its source. Code-index hits get a
|
|
38
38
|
* ` [code]` tag; memory records are left bare so a memory-only line renders
|
|
39
|
-
* byte-identically to the
|
|
40
|
-
* `injectedIds`; a missing/short entry
|
|
39
|
+
* byte-identically to the output from before code-index hits existed.
|
|
40
|
+
* `injectedSources` is parallel to `injectedIds`; a missing/short entry
|
|
41
|
+
* defaults to memory (no tag).
|
|
41
42
|
*/
|
|
42
43
|
function labelInjectedIds(entry: TurnInjectionEntry): string {
|
|
43
44
|
const sources = entry.injectedSources ?? [];
|
|
@@ -107,7 +108,7 @@ export function buildTurnInjectionsText(agentId: string, entries: readonly TurnI
|
|
|
107
108
|
|
|
108
109
|
/**
|
|
109
110
|
* Render the MAIN interactive session's per-turn injection ring
|
|
110
|
-
* (`Orchestrator.getTurnInjections()
|
|
111
|
+
* (`Orchestrator.getTurnInjections()`), most-recent-turn first, with the
|
|
111
112
|
* same honest empty state as the per-agent path. The main-session counterpart
|
|
112
113
|
* to {@link buildTurnInjectionsText}: no agent id, main-session-appropriate
|
|
113
114
|
* wording, but the identical per-entry rendering (`formatTurnInjectionEntry`).
|