@pellux/goodvibes-tui 1.16.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5490 -1806
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/management-commands.ts +82 -15
- package/src/cli/management.ts +0 -2
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +18 -9
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/memory-provenance.ts +67 -0
- package/src/core/pairing-devices.ts +49 -0
- package/src/core/pairing-handoff.ts +101 -0
- package/src/core/pairing-offers.ts +83 -0
- package/src/core/pairing-origin.ts +78 -0
- package/src/core/pairing-tailscale-gateway.ts +56 -0
- package/src/core/power-chip-source.ts +91 -0
- package/src/core/power-status.ts +95 -0
- package/src/core/stable-host.ts +171 -0
- package/src/core/stream-event-wiring.ts +29 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +40 -1
- package/src/core/turn-event-wiring.ts +14 -8
- package/src/daemon/cli.ts +64 -58
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +55 -9
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +109 -0
- package/src/input/commands/git-runtime.ts +28 -4
- package/src/input/commands/graph-runtime.ts +45 -0
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +46 -27
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -2
- package/src/input/config-modal-types.ts +14 -3
- package/src/input/config-modal.ts +75 -13
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +29 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +21 -3
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +52 -57
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +27 -17
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +90 -90
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +83 -11
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +16 -4
- package/src/panels/builtin-modals.ts +59 -42
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +39 -6
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +163 -11
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +73 -57
- package/src/panels/fleet-read-model.ts +84 -14
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +41 -5
- package/src/panels/fleet-tabs.ts +15 -16
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +153 -9
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +7 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +23 -5
- package/src/renderer/settings-modal.ts +129 -39
- package/src/renderer/shell-surface.ts +6 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +13 -7
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +38 -1
- package/src/runtime/bootstrap-command-context.ts +7 -2
- package/src/runtime/bootstrap-command-parts.ts +9 -5
- package/src/runtime/bootstrap-core.ts +41 -41
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +13 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +90 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +6 -21
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +113 -113
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +6 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +108 -132
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
|
@@ -5,7 +5,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Parse `--target <id>[:<kind>]` out of the args following `<panel-id>
|
|
8
|
-
* [top|bottom]` (
|
|
8
|
+
* [top|bottom]` (item 4). Splits the args array in place (removing the
|
|
9
9
|
* flag + its value) so positional `pane` parsing downstream is unaffected by
|
|
10
10
|
* where the flag appears. Only `open fleet` currently has a deep-link
|
|
11
11
|
* consumer (FleetPanel.receiveDeepLink) — other panel ids just ignore an
|
|
@@ -32,7 +32,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
|
|
|
32
32
|
const pm = requirePanelManager(ctx);
|
|
33
33
|
const sub = args[0]?.toLowerCase() ?? '';
|
|
34
34
|
if (!sub || sub === 'toggle') {
|
|
35
|
-
//
|
|
35
|
+
// (the purge): 'panel-list' (the browse-all-panels picker) was
|
|
36
36
|
// DELETE-disposition — a picker over a handful of panels is dead
|
|
37
37
|
// weight now (see the Ctrl+P selectionModal repoint in
|
|
38
38
|
// shell/ui-openers.ts for its interactive replacement). Bare
|
|
@@ -71,9 +71,9 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
|
|
|
71
71
|
// no panel lands in the workspace. Report that honestly rather than
|
|
72
72
|
// claiming "Panel opened: <id>".
|
|
73
73
|
const redirectTarget = pm.getModalRedirect(id);
|
|
74
|
-
//
|
|
74
|
+
// focus rule 1a: the command path leaves focus in the composer
|
|
75
75
|
// ("mid-command-flow") — showPanel does not grab panel focus here.
|
|
76
|
-
//
|
|
76
|
+
// forward the deep-link target so the panel lands on the row.
|
|
77
77
|
if (ctx.showPanel) ctx.showPanel(id, pane as 'top' | 'bottom' | undefined, target);
|
|
78
78
|
else {
|
|
79
79
|
pm.open(id, pane as 'top' | 'bottom' | undefined, target);
|
|
@@ -169,7 +169,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
|
|
|
169
169
|
} else {
|
|
170
170
|
const id = args[0]!;
|
|
171
171
|
try {
|
|
172
|
-
//
|
|
172
|
+
// bare `/panel <id>` is the same command path as `/panel open
|
|
173
173
|
// <id>` — composer stays focused.
|
|
174
174
|
if (ctx.showPanel) ctx.showPanel(id);
|
|
175
175
|
else {
|
|
@@ -27,6 +27,7 @@ import { join } from 'node:path';
|
|
|
27
27
|
import { createGoodVibesSdk } from '@pellux/goodvibes-sdk';
|
|
28
28
|
import type { GoodVibesSdk } from '@pellux/goodvibes-sdk';
|
|
29
29
|
import { GoodVibesSdkError } from '@pellux/goodvibes-sdk';
|
|
30
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
30
31
|
import { resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
|
|
31
32
|
import { getOrCreateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
32
33
|
import type { CommandContext } from '../command-registry.ts';
|
|
@@ -44,8 +45,7 @@ export interface OperatorRpcAvailable {
|
|
|
44
45
|
|
|
45
46
|
export type OperatorRpc = OperatorRpcUnavailable | OperatorRpcAvailable;
|
|
46
47
|
|
|
47
|
-
function resolveControlPlaneBaseUrl(
|
|
48
|
-
const configManager = context.platform.configManager;
|
|
48
|
+
function resolveControlPlaneBaseUrl(configManager: ConfigManager): string | null {
|
|
49
49
|
const explicit = String(configManager.get('controlPlane.baseUrl') ?? '').trim();
|
|
50
50
|
if (explicit) return explicit;
|
|
51
51
|
const host = String(configManager.get('controlPlane.host') ?? '').trim();
|
|
@@ -55,27 +55,48 @@ function resolveControlPlaneBaseUrl(context: CommandContext): string | null {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
* this workspace's control-plane
|
|
60
|
-
*
|
|
61
|
-
*
|
|
58
|
+
* Context-free core of {@link getOperatorRpc}: resolve (or honestly refuse to
|
|
59
|
+
* resolve) an operator SDK client wired to this workspace's control-plane
|
|
60
|
+
* daemon from just the two things the resolution actually needs — the config
|
|
61
|
+
* manager and the home directory. Shared by the command layer (via
|
|
62
|
+
* getOperatorRpc) and the Fleet panel's gateway (fleet-gateway.ts) so both
|
|
63
|
+
* reach the same daemon over the same generic invoke path, never two divergent
|
|
64
|
+
* clients. Refusal reasons are surfaced verbatim so callers print them directly.
|
|
62
65
|
*/
|
|
63
|
-
export function
|
|
64
|
-
const configManager =
|
|
66
|
+
export function resolveOperatorRpc(deps: { readonly configManager: ConfigManager; readonly homeDirectory: string | (() => string) }): OperatorRpc {
|
|
67
|
+
const { configManager } = deps;
|
|
65
68
|
if (!resolveDaemonEnabled(configManager)) {
|
|
66
69
|
return { available: false, reason: 'the daemon is disabled (daemon.enabled=false) — no operator surface to reach. Enable it in /settings, then retry.' };
|
|
67
70
|
}
|
|
68
|
-
const baseUrl = resolveControlPlaneBaseUrl(
|
|
71
|
+
const baseUrl = resolveControlPlaneBaseUrl(configManager);
|
|
69
72
|
if (!baseUrl) {
|
|
70
73
|
return { available: false, reason: 'no control-plane base URL is configured (controlPlane.baseUrl / controlPlane.host+port) — cannot reach the operator surface.' };
|
|
71
74
|
}
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
// Resolve the home directory only AFTER the static refusals — a caller whose
|
|
76
|
+
// shell paths are not wired (a disabled-daemon path) must still get the honest
|
|
77
|
+
// unavailable reason above, never a "shell paths not wired" throw.
|
|
78
|
+
const homeDirectory = typeof deps.homeDirectory === 'function' ? deps.homeDirectory() : deps.homeDirectory;
|
|
79
|
+
const daemonHomeDir = join(homeDirectory, '.goodvibes', 'daemon');
|
|
74
80
|
const token = getOrCreateCompanionToken('tui', { daemonHomeDir }).token;
|
|
75
81
|
const sdk = createGoodVibesSdk({ baseUrl, authToken: token });
|
|
76
82
|
return { available: true, sdk };
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Resolve (or honestly refuse to resolve) an operator SDK client wired to
|
|
87
|
+
* this workspace's control-plane daemon. Reasons for refusal are surfaced
|
|
88
|
+
* verbatim to the caller so `/ci`, `/checkin`, and `/principals` can print
|
|
89
|
+
* them directly rather than inventing their own wording.
|
|
90
|
+
*/
|
|
91
|
+
export function getOperatorRpc(context: CommandContext): OperatorRpc {
|
|
92
|
+
return resolveOperatorRpc({
|
|
93
|
+
configManager: context.platform.configManager,
|
|
94
|
+
// Lazy: only resolved once the daemon-enabled + base-URL refusals pass, so a
|
|
95
|
+
// disabled-daemon context with no shell paths still returns the honest reason.
|
|
96
|
+
homeDirectory: () => requireShellPaths(context).homeDirectory,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
79
100
|
/**
|
|
80
101
|
* Render a thrown operator-rpc error honestly: a 404 means this daemon has
|
|
81
102
|
* not wired a handler for the verb (a live local runtime that simply doesn't
|
|
@@ -63,7 +63,7 @@ const PERMISSION_KEYS: readonly { readonly key: string; readonly label: string;
|
|
|
63
63
|
{ key: 'permissions.mode', label: 'Permission mode', note: 'also the current session mode' },
|
|
64
64
|
{ key: 'permissions.backgroundAgents', label: 'Background-agent mode' },
|
|
65
65
|
{ key: 'behavior.autoApprove', label: 'Auto-approve everything' },
|
|
66
|
-
{ key: '
|
|
66
|
+
{ key: 'permissions.engine', label: 'Permission evaluator', note: 'baseline or policy-engine' },
|
|
67
67
|
{ key: 'permissions.tools.read', label: 'Tool rule: read' },
|
|
68
68
|
{ key: 'permissions.tools.write', label: 'Tool rule: write' },
|
|
69
69
|
{ key: 'permissions.tools.edit', label: 'Tool rule: edit' },
|
|
@@ -1,21 +1,112 @@
|
|
|
1
|
-
import type { CommandRegistry } from '../command-registry.ts';
|
|
1
|
+
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
2
|
+
import type { SelectionItem, SelectionAction } from '../selection-modal.ts';
|
|
2
3
|
import { buildPermissionProvenance, renderPermissionProvenance } from './permissions-provenance.ts';
|
|
4
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
|
+
|
|
6
|
+
/** One remembered-rule row: what it does, and how/when it was remembered. */
|
|
7
|
+
function describeRuleRow(record: {
|
|
8
|
+
rule: { id: string; description?: string | undefined; effect: 'allow' | 'deny' };
|
|
9
|
+
tier: string;
|
|
10
|
+
tool: string;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
}): { label: string; detail: string } {
|
|
13
|
+
const what = record.rule.description ?? record.rule.id;
|
|
14
|
+
return {
|
|
15
|
+
label: `${record.tool}: ${what}`,
|
|
16
|
+
detail: `${record.rule.effect} · remembered ${record.tier} · ${new Date(record.createdAt).toLocaleString()}`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function showRememberedRules(ctx: CommandContext): void {
|
|
21
|
+
const store = ctx.clients?.userPermissionRuleStore;
|
|
22
|
+
if (!store) {
|
|
23
|
+
ctx.print('Remembered permission rules are not available on this surface.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const records = store.list();
|
|
27
|
+
if (records.length === 0) {
|
|
28
|
+
ctx.print('No remembered permission rules. Approving an ask with a remember tier stores one here.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (ctx.openSelection) {
|
|
32
|
+
const revoke = new Map<string, SelectionAction>([['d', 'delete']]);
|
|
33
|
+
const items: SelectionItem[] = records.map((record) => {
|
|
34
|
+
const { label, detail } = describeRuleRow(record);
|
|
35
|
+
return { id: record.rule.id, label, detail, actions: '[d] revoke' };
|
|
36
|
+
});
|
|
37
|
+
ctx.openSelection('Remembered permission rules', items, { allowSearch: true, customActions: revoke }, (result) => {
|
|
38
|
+
if (!result || result.action !== 'delete') return;
|
|
39
|
+
void (async () => {
|
|
40
|
+
try {
|
|
41
|
+
const removed = await store.delete(result.item.id);
|
|
42
|
+
ctx.print(removed ? `Revoked remembered rule: ${result.item.label}` : `Rule already gone: ${result.item.label}`);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
ctx.print(`Could not revoke the rule: ${summarizeError(error)}`);
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// No selection surface: list them honestly with the revoke command to run.
|
|
51
|
+
ctx.print([
|
|
52
|
+
'Remembered permission rules:',
|
|
53
|
+
...records.map((record) => {
|
|
54
|
+
const { label, detail } = describeRuleRow(record);
|
|
55
|
+
return ` ${record.rule.id} ${label} (${detail})`;
|
|
56
|
+
}),
|
|
57
|
+
'',
|
|
58
|
+
'Revoke one with: /permissions revoke <rule-id>',
|
|
59
|
+
].join('\n'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function revokeRememberedRule(ctx: CommandContext, ruleId: string): Promise<void> {
|
|
63
|
+
const store = ctx.clients?.userPermissionRuleStore;
|
|
64
|
+
if (!store) {
|
|
65
|
+
ctx.print('Remembered permission rules are not available on this surface.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!ruleId) {
|
|
69
|
+
ctx.print('Usage: /permissions revoke <rule-id> (see /permissions rules for ids)');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const removed = await store.delete(ruleId);
|
|
74
|
+
ctx.print(removed ? `Revoked remembered rule: ${ruleId}` : `No remembered rule with id: ${ruleId}`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
ctx.print(`Could not revoke the rule: ${summarizeError(error)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
3
79
|
|
|
4
80
|
/**
|
|
5
|
-
* `/permissions` — the permission
|
|
6
|
-
* permission-relevant setting in effect and where each value came from
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
9
|
-
*
|
|
81
|
+
* `/permissions` — the permission settings surface. With no arguments it prints
|
|
82
|
+
* every permission-relevant setting in effect and where each value came from
|
|
83
|
+
* (provenance; read-only). `rules` lists the durable remembered approvals with a
|
|
84
|
+
* press-Enter revoke; `revoke <id>` removes one by id. Revokes hit the same
|
|
85
|
+
* in-process rule store the evaluator reads, so they take effect live.
|
|
10
86
|
*/
|
|
11
87
|
export function registerPermissionsRuntimeCommands(registry: CommandRegistry): void {
|
|
12
88
|
registry.register({
|
|
13
89
|
name: 'permissions',
|
|
14
90
|
aliases: ['perms'],
|
|
15
|
-
description: 'Show permission settings
|
|
16
|
-
|
|
91
|
+
description: 'Show permission settings and provenance, and list or revoke remembered approval rules',
|
|
92
|
+
usage: '[rules | revoke <rule-id>]',
|
|
93
|
+
argsHint: '[rules|revoke]',
|
|
94
|
+
async handler(args, ctx) {
|
|
95
|
+
const sub = (args[0] ?? '').toLowerCase();
|
|
96
|
+
if (sub === 'rules' || sub === 'rule') {
|
|
97
|
+
showRememberedRules(ctx);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (sub === 'revoke' || sub === 'forget') {
|
|
101
|
+
await revokeRememberedRule(ctx, args[1] ?? '');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
17
104
|
const provenance = buildPermissionProvenance(ctx.platform.configManager);
|
|
18
105
|
ctx.print(renderPermissionProvenance(provenance));
|
|
106
|
+
const ruleCount = ctx.clients?.userPermissionRuleStore?.list().length ?? 0;
|
|
107
|
+
ctx.print(ruleCount > 0
|
|
108
|
+
? `\n${ruleCount} remembered approval rule${ruleCount === 1 ? '' : 's'} in effect. Use /permissions rules to review or revoke them.`
|
|
109
|
+
: '\nNo remembered approval rules. Use /permissions rules once you have some.');
|
|
19
110
|
},
|
|
20
111
|
});
|
|
21
112
|
}
|
|
@@ -13,7 +13,7 @@ import { togglePlanMode, permissionModeLabel, type PermissionModeValue } from '.
|
|
|
13
13
|
* A lone one of these is refused rather than seeded as a goal, so a stray verb
|
|
14
14
|
* can never overwrite the project goal with itself.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* `dismiss` and `answer` are now REAL subcommands (handled above this
|
|
17
17
|
* guard), so they were removed from the refuse-list. `pause`/`stop`/`cancel`
|
|
18
18
|
* remain here — they still have no backing verb and must not seed a goal.
|
|
19
19
|
*/
|
|
@@ -178,7 +178,7 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
|
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
//
|
|
181
|
+
// /plan dismiss — archive the current plan. Dismisses the active
|
|
182
182
|
// execution plan (ExecutionPlanManager.dismiss, honest per-state) AND
|
|
183
183
|
// deactivates the project-planning interview state shown in the modal so a
|
|
184
184
|
// later /plan <goal> starts fresh. Mid-execution is refused outright.
|
|
@@ -223,7 +223,7 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
|
|
|
223
223
|
return;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
//
|
|
226
|
+
// /plan answer <n|question-id> <text> — record a real answer to an
|
|
227
227
|
// open planning question (moves open → answered, consumed on next refine).
|
|
228
228
|
if (args[0] === 'answer') {
|
|
229
229
|
if (!projectPlanningService || !projectId) {
|
|
@@ -98,7 +98,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
|
|
|
98
98
|
const shellPaths = requireShellPaths(ctx);
|
|
99
99
|
const sub = args[0] ?? 'open';
|
|
100
100
|
if (sub === 'open' || sub === 'panel') {
|
|
101
|
-
ctx.openModal?.('sandbox-modal'); //
|
|
101
|
+
ctx.openModal?.('sandbox-modal'); // sandbox panel -> config modal
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
if (sub === 'review') {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CommandRegistry } from '../command-registry.ts';
|
|
2
|
+
import { powerStatusLines } from '../../core/power-status.ts';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// /power — host sleep ownership (the ops/status idiom surface for power.*).
|
|
6
|
+
//
|
|
7
|
+
// Shows the honest account of sleep ownership — the "sleep disabled" chip
|
|
8
|
+
// meaning, each "held because X" work-inhibition reason, and the lid-split note
|
|
9
|
+
// rendered VERBATIM when the OS could block idle-sleep but not lid-close
|
|
10
|
+
// suspend — and toggles the daemon-held keep-awake switch. Owner ruling: the
|
|
11
|
+
// always-visible chip IS the safety mechanism, so there is no timer and no
|
|
12
|
+
// AC-only option — keep-awake is exactly one on/off toggle.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export function registerPowerRuntimeCommands(registry: CommandRegistry): void {
|
|
16
|
+
registry.register({
|
|
17
|
+
name: 'power',
|
|
18
|
+
description: 'Host sleep ownership — show status and toggle keep-awake',
|
|
19
|
+
usage: '[status | on | off | toggle]',
|
|
20
|
+
argsHint: '[status|on|off|toggle]',
|
|
21
|
+
async handler(args, ctx) {
|
|
22
|
+
const sub = (args[0] ?? 'status').toLowerCase();
|
|
23
|
+
|
|
24
|
+
if (sub === 'status' || args.length === 0) {
|
|
25
|
+
const state = ctx.getPowerState?.();
|
|
26
|
+
if (!state) {
|
|
27
|
+
ctx.print('Power status is unavailable in this session.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const lines = ['Host sleep ownership:', ...powerStatusLines(state).map((l) => ` ${l}`)];
|
|
31
|
+
lines.push(state.keepAwake
|
|
32
|
+
? 'Keep-awake is ON. Turn it off with /power off.'
|
|
33
|
+
: 'Keep-awake is OFF. Turn it on with /power on.');
|
|
34
|
+
ctx.print(lines.join('\n'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (sub === 'on' || sub === 'off' || sub === 'toggle') {
|
|
39
|
+
if (!ctx.setKeepAwake || !ctx.getPowerState) {
|
|
40
|
+
ctx.print('Keep-awake cannot be changed in this session.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const current = ctx.getPowerState().keepAwake;
|
|
44
|
+
const next = sub === 'on' ? true : sub === 'off' ? false : !current;
|
|
45
|
+
const state = await ctx.setKeepAwake(next);
|
|
46
|
+
const lines = [state.keepAwake ? 'Keep-awake ON — this host will not idle-sleep.' : 'Keep-awake OFF.'];
|
|
47
|
+
// Surface the honest lid-split note verbatim if the SDK served one.
|
|
48
|
+
if (state.note) lines.push(` ${state.note}`);
|
|
49
|
+
ctx.print(lines.join('\n'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ctx.print('Usage: /power [status | on | off | toggle]');
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -3,11 +3,13 @@ import type {
|
|
|
3
3
|
ProviderAccountRecord,
|
|
4
4
|
ProviderAccountSnapshot,
|
|
5
5
|
} from '@/runtime/index.ts';
|
|
6
|
+
import type { SelectionItem } from '../selection-modal.ts';
|
|
6
7
|
import {
|
|
7
|
-
openCommandPanel,
|
|
8
8
|
requireOperatorClient,
|
|
9
9
|
} from './runtime-services.ts';
|
|
10
10
|
|
|
11
|
+
type RecommendedAction = ProviderAccountRecord['recommendedActions'][number];
|
|
12
|
+
|
|
11
13
|
async function loadProviderAccountSnapshot(context: CommandContext): Promise<ProviderAccountSnapshot> {
|
|
12
14
|
return await requireOperatorClient(context).providers.accountSnapshot();
|
|
13
15
|
}
|
|
@@ -20,6 +22,71 @@ function findProviderAccountRecord(
|
|
|
20
22
|
return snapshot.providers.find((entry) => entry.providerId === providerId);
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Present a record's recommended actions as executable rows. A recommended
|
|
27
|
+
* action that carries a structured command becomes a press-Enter row that RUNS
|
|
28
|
+
* that command through the command registry — never a line the user has to
|
|
29
|
+
* retype. Actions with no command are genuinely out-of-product steps, so they
|
|
30
|
+
* stay as plain (non-executing) rows. When there is no interactive selection
|
|
31
|
+
* surface, each action is printed honestly instead.
|
|
32
|
+
*/
|
|
33
|
+
export function presentRecommendedActions(
|
|
34
|
+
ctx: CommandContext,
|
|
35
|
+
providerId: string,
|
|
36
|
+
actions: readonly RecommendedAction[],
|
|
37
|
+
): void {
|
|
38
|
+
if (actions.length === 0) {
|
|
39
|
+
ctx.print('No active repair actions suggested.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (ctx.openSelection) {
|
|
43
|
+
const items: SelectionItem[] = actions.map((action, index) => {
|
|
44
|
+
if (action.command) {
|
|
45
|
+
const argsPart = action.command.args.length > 0 ? ` ${action.command.args.join(' ')}` : '';
|
|
46
|
+
return {
|
|
47
|
+
id: String(index),
|
|
48
|
+
label: action.description,
|
|
49
|
+
detail: `runs /${action.command.name}${argsPart}`,
|
|
50
|
+
primaryAction: 'select',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
id: String(index),
|
|
55
|
+
label: action.description,
|
|
56
|
+
detail: 'manual step (nothing to run)',
|
|
57
|
+
primaryAction: 'select',
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
ctx.openSelection(
|
|
61
|
+
`Repair ${providerId}`,
|
|
62
|
+
items,
|
|
63
|
+
{ allowSearch: false, primaryVerbLabel: 'Run' },
|
|
64
|
+
(result) => {
|
|
65
|
+
if (!result) return;
|
|
66
|
+
const action = actions[Number(result.item.id)];
|
|
67
|
+
if (!action) return;
|
|
68
|
+
if (action.command) {
|
|
69
|
+
const args = [...action.command.args];
|
|
70
|
+
void (ctx.executeCommand?.(action.command.name, args)
|
|
71
|
+
?? Promise.resolve(ctx.print(`Cannot run /${action.command.name} — command execution is unavailable here.`)));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
ctx.print(action.description);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// No selection surface (headless/command-only): list actions honestly.
|
|
80
|
+
ctx.print([
|
|
81
|
+
'Recommended actions:',
|
|
82
|
+
...actions.map((action) => (
|
|
83
|
+
action.command
|
|
84
|
+
? ` • ${action.description} — /${action.command.name}${action.command.args.length > 0 ? ` ${action.command.args.join(' ')}` : ''}`
|
|
85
|
+
: ` • ${action.description} (manual step)`
|
|
86
|
+
)),
|
|
87
|
+
].join('\n'));
|
|
88
|
+
}
|
|
89
|
+
|
|
23
90
|
export function registerProviderAccountsRuntimeCommands(registry: CommandRegistry): void {
|
|
24
91
|
registry.register({
|
|
25
92
|
name: 'accounts',
|
|
@@ -29,7 +96,7 @@ export function registerProviderAccountsRuntimeCommands(registry: CommandRegistr
|
|
|
29
96
|
async handler(args, ctx) {
|
|
30
97
|
const sub = (args[0] ?? 'review').toLowerCase();
|
|
31
98
|
if (sub === 'panel' || sub === 'open') {
|
|
32
|
-
ctx.openModal?.('providers-modal'); //
|
|
99
|
+
ctx.openModal?.('providers-modal'); // accounts alias -> provider-health modal
|
|
33
100
|
return;
|
|
34
101
|
}
|
|
35
102
|
const snapshot = await loadProviderAccountSnapshot(ctx);
|
|
@@ -63,10 +130,8 @@ export function registerProviderAccountsRuntimeCommands(registry: CommandRegistr
|
|
|
63
130
|
` preferred route: ${record.preferredRoute}`,
|
|
64
131
|
...(record.fallbackRisk ? [` fallback: ${record.fallbackRisk}`] : []),
|
|
65
132
|
...(record.issues.map((issue) => ` issue: ${issue}`)),
|
|
66
|
-
...(record.recommendedActions.length > 0
|
|
67
|
-
? [' next:', ...record.recommendedActions.map((action) => ` ${action}`)]
|
|
68
|
-
: [' No active repair actions suggested.']),
|
|
69
133
|
].join('\n'));
|
|
134
|
+
presentRecommendedActions(ctx, record.providerId, record.recommendedActions);
|
|
70
135
|
return;
|
|
71
136
|
}
|
|
72
137
|
if (sub === 'show') {
|
|
@@ -95,8 +160,8 @@ export function registerProviderAccountsRuntimeCommands(registry: CommandRegistr
|
|
|
95
160
|
...record.usageWindows.map((entry) => ` window: ${entry.label} — ${entry.detail}`),
|
|
96
161
|
...record.issues.map((issue) => ` issue: ${issue}`),
|
|
97
162
|
...record.notes.map((note) => ` note: ${note}`),
|
|
98
|
-
...record.recommendedActions.map((action) => ` next: ${action}`),
|
|
99
163
|
].join('\n'));
|
|
164
|
+
presentRecommendedActions(ctx, record.providerId, record.recommendedActions);
|
|
100
165
|
return;
|
|
101
166
|
}
|
|
102
167
|
ctx.print([
|
|
@@ -15,14 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { SlashCommand, CommandContext } from '../command-registry.ts';
|
|
18
|
-
import type { ConfigKey } from '../../config/index.ts';
|
|
19
18
|
import type { RouteExplanation } from '@pellux/goodvibes-sdk/platform/providers';
|
|
20
19
|
import type { FallbackTestResult, FallbackTransition } from '@pellux/goodvibes-sdk/platform/providers';
|
|
21
20
|
import type { ProviderApiModelRecord } from '@pellux/goodvibes-sdk/platform/providers';
|
|
22
21
|
import { requireProviderApi } from './runtime-services.ts';
|
|
22
|
+
import { featureEnablementWrite } from '../../runtime/feature-settings.ts';
|
|
23
23
|
|
|
24
24
|
const PROVIDER_OPTIMIZER_FLAG = 'provider-optimizer';
|
|
25
|
-
const PROVIDER_OPTIMIZER_CONFIG_KEY = `featureFlags.${PROVIDER_OPTIMIZER_FLAG}` as ConfigKey;
|
|
26
25
|
|
|
27
26
|
// ---------------------------------------------------------------------------
|
|
28
27
|
// Formatting helpers
|
|
@@ -95,9 +94,12 @@ function handleOptimizerToggle(
|
|
|
95
94
|
const wasEnabled = optimizer.enabled;
|
|
96
95
|
optimizer.setEnabled(enable);
|
|
97
96
|
|
|
98
|
-
// Persist to config so the setting survives restart
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
// Persist to config so the setting survives restart: the optimizer is
|
|
98
|
+
// governed by the provider.optimizerMode settings key (off = disabled;
|
|
99
|
+
// manual/auto/pinned = active), and the settings bridge forwards the same
|
|
100
|
+
// write to the runtime gate.
|
|
101
|
+
const write = featureEnablementWrite(PROVIDER_OPTIMIZER_FLAG, enable);
|
|
102
|
+
if (write) context.platform.configManager.setDynamic(write.key, write.value);
|
|
101
103
|
|
|
102
104
|
if (enable && !wasEnabled) {
|
|
103
105
|
context.print('[provider] Optimizer enabled.');
|
|
@@ -1,36 +1,31 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
1
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
3
|
-
import { openModalCommand
|
|
4
|
-
import { regenerateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
2
|
+
import { openModalCommand } from './runtime-services.ts';
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* Register the /qrcode command.
|
|
8
6
|
*
|
|
9
|
-
* `/qrcode` (no args) opens the
|
|
10
|
-
* token
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* `/qrcode` (no args, aliases /qr /pair) opens the device-pairing modal: a QR of
|
|
8
|
+
* the `#pair=<token>` deep link plus the offer set. Each open mints its own
|
|
9
|
+
* named per-device token, so `/qrcode regenerate` no longer rotates a shared
|
|
10
|
+
* token — it simply re-opens the modal, which mints a fresh token and QR. The
|
|
11
|
+
* previously-shown token stays valid until you revoke it in
|
|
12
|
+
* /settings → security → devices.
|
|
13
13
|
*/
|
|
14
14
|
export function registerQrcodeRuntimeCommands(registry: CommandRegistry): void {
|
|
15
15
|
registry.register({
|
|
16
16
|
name: 'qrcode',
|
|
17
17
|
aliases: ['qr', 'pair'],
|
|
18
|
-
description: 'Open the
|
|
18
|
+
description: 'Open the device-pairing modal (QR deep link + offers); each open mints a fresh device token',
|
|
19
19
|
usage: '[regenerate]',
|
|
20
20
|
argsHint: '[regenerate]',
|
|
21
21
|
handler(args, ctx) {
|
|
22
22
|
if (args[0]?.toLowerCase() === 'regenerate') {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`re-scan the QR (new peer ${rotated.peerId.slice(0, 8)}…) to reconnect the companion app.`,
|
|
30
|
-
);
|
|
31
|
-
} catch (error) {
|
|
32
|
-
ctx.print(`Could not regenerate the pairing token: ${String(error)}`);
|
|
33
|
-
}
|
|
23
|
+
// Each pairing-modal open mints its own token, so "regenerate" is just a
|
|
24
|
+
// fresh open: the re-pull mints a new named token + QR in place. The
|
|
25
|
+
// prior token is not rotated out from under a live device — revoke it
|
|
26
|
+
// explicitly in /settings → security → devices when you no longer need it.
|
|
27
|
+
ctx.print('Opening a fresh pairing QR — it mints a new device token. Revoke old ones in /settings → security → devices.');
|
|
28
|
+
openModalCommand(ctx, 'pairing-modal');
|
|
34
29
|
return;
|
|
35
30
|
}
|
|
36
31
|
openModalCommand(ctx, 'pairing-modal');
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CommandRegistry } from '../command-registry.ts';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// /queue — the mid-turn message queue as an editable list.
|
|
5
|
+
//
|
|
6
|
+
// Messages typed while a turn is streaming are QUEUED (they deliver after the
|
|
7
|
+
// turn). Until a message is delivered it stays editable and deletable via the
|
|
8
|
+
// SDK's editQueuedMessage / deleteQueuedMessage verbs; a delivered message has
|
|
9
|
+
// left the queue and is no longer listed — delivery is immutability. This
|
|
10
|
+
// command is the editable interface over the same list the composer renders.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export function registerQueueRuntimeCommands(registry: CommandRegistry): void {
|
|
14
|
+
registry.register({
|
|
15
|
+
name: 'queue',
|
|
16
|
+
aliases: ['q'],
|
|
17
|
+
description: 'Mid-turn message queue — list, edit, or delete still-undelivered messages',
|
|
18
|
+
usage: '[list | edit <n> <text> | delete <n>]',
|
|
19
|
+
argsHint: '[list|edit <n> <text>|delete <n>]',
|
|
20
|
+
handler(args, ctx) {
|
|
21
|
+
const list = ctx.listQueuedMessages?.() ?? [];
|
|
22
|
+
const sub = (args[0] ?? 'list').toLowerCase();
|
|
23
|
+
|
|
24
|
+
if (sub === 'list' || args.length === 0) {
|
|
25
|
+
if (list.length === 0) {
|
|
26
|
+
ctx.print('No queued messages. (Messages typed while a turn is streaming queue here until it finishes.)');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const lines = [`Queued messages (${list.length}) — editable until delivered:`];
|
|
30
|
+
list.forEach((m, i) => lines.push(` ${i + 1}. ${m.text}`));
|
|
31
|
+
lines.push('Use /queue edit <n> <new text> or /queue delete <n>.');
|
|
32
|
+
ctx.print(lines.join('\n'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Both edit and delete address a message by its 1-based list position.
|
|
37
|
+
const n = Number.parseInt(args[1] ?? '', 10);
|
|
38
|
+
if (!Number.isInteger(n) || n < 1 || n > list.length) {
|
|
39
|
+
ctx.print(list.length === 0
|
|
40
|
+
? 'No queued messages to act on.'
|
|
41
|
+
: `No queued message #${args[1] ?? ''}. Valid range: 1..${list.length}. Run /queue to list them.`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const target = list[n - 1]!;
|
|
45
|
+
|
|
46
|
+
if (sub === 'delete') {
|
|
47
|
+
const ok = ctx.deleteQueuedMessage?.(target.id) ?? false;
|
|
48
|
+
ctx.print(ok
|
|
49
|
+
? `Deleted queued message #${n}.`
|
|
50
|
+
: `Could not delete message #${n} — it was already delivered (delivered messages are immutable).`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (sub === 'edit') {
|
|
55
|
+
const text = args.slice(2).join(' ');
|
|
56
|
+
if (!text.trim()) {
|
|
57
|
+
ctx.print('Usage: /queue edit <n> <new text>. Provide the replacement text.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const ok = ctx.editQueuedMessage?.(target.id, text) ?? false;
|
|
61
|
+
ctx.print(ok
|
|
62
|
+
? `Edited queued message #${n}.`
|
|
63
|
+
: `Could not edit message #${n} — it was already delivered (delivered messages are immutable).`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ctx.print('Usage: /queue [list | edit <n> <text> | delete <n>]');
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|