@timurproko/a1 0.2.7-dev.801 → 0.2.7-dev.808
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/dist/app/session-shell/session-shell.js +12 -1
- package/dist/app/session-shell/shell-host.js +7 -4
- package/dist/composition/owned-ui.d.ts +2 -0
- package/dist/composition/owned-ui.js +61 -11
- package/dist/composition/settings-route-host.d.ts +6 -1
- package/dist/composition/settings-route-host.js +7 -6
- package/dist/features/owned-ui/settings-app.d.ts +4 -1
- package/dist/features/owned-ui/settings-app.js +9 -3
- package/dist/features/resident-tab/shortcut-declarations.js +1 -1
- package/dist/features/resident-tab/status-deriver.d.ts +4 -3
- package/dist/features/resident-tab/status-deriver.js +2 -5
- package/dist/foundation/release/bootstrap.js +2 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +1 -1
- package/dist/foundation/resident-tabs/bridge-client.js +2 -1
- package/dist/foundation/terminal-cleanup/index.d.ts +1 -1
- package/dist/foundation/terminal-cleanup/index.js +1 -1
- package/dist/foundation/terminal-cleanup/terminal-reset.d.ts +5 -0
- package/dist/foundation/terminal-cleanup/terminal-reset.js +5 -0
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +6 -0
- package/dist/integrations/pi/tui-runtime/adapter.js +28 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/ui/settings/manager.d.ts +6 -0
- package/dist/ui/settings/manager.js +25 -0
- package/docs/architecture/resident-tabs.md +7 -6
- package/package.json +1 -1
|
@@ -1762,7 +1762,18 @@ export class OwnedUiSessionPresenter {
|
|
|
1762
1762
|
this.#dialogId = dialog.id;
|
|
1763
1763
|
}
|
|
1764
1764
|
openOwnedRoute(route) {
|
|
1765
|
-
this.#
|
|
1765
|
+
if (this.#disposed)
|
|
1766
|
+
return;
|
|
1767
|
+
// Invariant: a route requested before the runtime runs (the resident Settings bridge route arrives
|
|
1768
|
+
// before start()) is retained, latest wins, and presented through the startup-route path once started.
|
|
1769
|
+
if (!this.#terminal.running) {
|
|
1770
|
+
this.#startupRoute = { route };
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const result = this.#openOwnedRoute(route);
|
|
1774
|
+
if (result.outcome !== "completed" && result.diagnostic !== null) {
|
|
1775
|
+
this.root.appendWorkflowMessage({ kind: "warning", message: result.diagnostic });
|
|
1776
|
+
}
|
|
1766
1777
|
}
|
|
1767
1778
|
hotkeysPresentation() {
|
|
1768
1779
|
const presentation = this.root.hotkeysPresentation();
|
|
@@ -4,7 +4,7 @@ import { piCanvasBackgroundAnsi } from "../../integrations/pi/components/theme.j
|
|
|
4
4
|
import { residentFixtureControl } from "../../foundation/resident-tabs/fixture-control.js";
|
|
5
5
|
import { residentProgressObserver } from "../../foundation/resident-tabs/progress-observation.js";
|
|
6
6
|
import { DamageAwareTerminalAdapter, } from "../../integrations/pi/tui-runtime/damage-aware-terminal.js";
|
|
7
|
-
import { PiTuiRuntimeAdapter } from "../../integrations/pi/tui-runtime/adapter.js";
|
|
7
|
+
import { PiTuiRuntimeAdapter, legacyKeyInput } from "../../integrations/pi/tui-runtime/adapter.js";
|
|
8
8
|
import { classifyPiTuiInput } from "../../integrations/pi/tui-runtime/input-presentation-coordinator.js";
|
|
9
9
|
import { pinnedLayoutRoot, } from "./session-shell-root.js";
|
|
10
10
|
/** The terminal a session's shell options call for: its layout, TUI mode, cursor, and presentation seams. */
|
|
@@ -348,7 +348,8 @@ export class OwnedUiTerminalHost {
|
|
|
348
348
|
callbacks.closed(rendered);
|
|
349
349
|
};
|
|
350
350
|
removeInterruptWatch = runtime.addInputListener(data => {
|
|
351
|
-
|
|
351
|
+
// Compatibility: a kitty-keyboard child reports Ctrl+C as a CSI-u sequence rather than the byte.
|
|
352
|
+
if (!attached() || !data.includes(INTERRUPT) && legacyKeyInput(data) !== INTERRUPT)
|
|
352
353
|
return undefined;
|
|
353
354
|
// Protocol: route hosts decide whether one interrupt closes their app. Raw-input forwarding
|
|
354
355
|
// is required because Pi handles Ctrl+C before the fullscreen overlay receives normal input.
|
|
@@ -406,8 +407,10 @@ export class OwnedUiTerminalHost {
|
|
|
406
407
|
const { events, rest } = parseMouseInput(data);
|
|
407
408
|
for (const event of events)
|
|
408
409
|
surface.handleMouse(event);
|
|
409
|
-
|
|
410
|
-
|
|
410
|
+
// Invariant: owned screens see legacy key bytes whether the terminal reports legacy or kitty keys.
|
|
411
|
+
const key = rest.length > 0 ? legacyKeyInput(rest) : "";
|
|
412
|
+
if (key.length > 0)
|
|
413
|
+
surface.handleInput(key);
|
|
411
414
|
if (surface.isClosed()) {
|
|
412
415
|
closeSurface(true);
|
|
413
416
|
return;
|
|
@@ -90,3 +90,5 @@ export declare function bindResidentTabBridge(tabBridge: TabBridgeClient, backen
|
|
|
90
90
|
readonly openRoute?: (route: "settings") => void;
|
|
91
91
|
}): Promise<() => void>;
|
|
92
92
|
export declare function correlateResidentTabSubmission(recovery: Pick<ResidentTabRecovery, "correlated" | "settled">, backend: Pick<ComposedSessionBackend, "identity" | "session">, submissionId: string, entryId: string): Promise<void>;
|
|
93
|
+
/** Rereads the shared settings document whenever its stat changes; returns the stop. */
|
|
94
|
+
export declare function watchSharedSettings(settings: Pick<OwnedSettingsManager, "file" | "refresh">): () => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unwatchFile, watchFile } from "node:fs";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { SuggestionDiagnosticCapture } from "../features/prompt-suggestions/diagnostics.js";
|
|
3
4
|
import { PRODUCT_IDENTITY } from "../product-identity.js";
|
|
@@ -188,13 +189,19 @@ export async function composeOwnedUi(options = {}) {
|
|
|
188
189
|
: await import("../features/owned-ui/release-note-state.js").then(module => module.claimReleaseNote({
|
|
189
190
|
configDir: productPaths.configDir, profileId: options.profileId, version: currentReleaseNote.version,
|
|
190
191
|
}));
|
|
191
|
-
const
|
|
192
|
+
const unsubscribeAccentChange = settings === null || !ownedSurfaces
|
|
192
193
|
? () => { }
|
|
193
194
|
: settings.onChange(current => {
|
|
194
195
|
const accent = current.value("accentColor");
|
|
195
196
|
host.setAccentColor(accent);
|
|
196
197
|
tabBridge?.accent(accent);
|
|
197
198
|
});
|
|
199
|
+
// Rationale: every resident tab is its own process over one shared settings document, so a
|
|
200
|
+
// change saved in any tab reaches the others only by rereading that document.
|
|
201
|
+
const unwatchSharedSettings = settings === null || !ownedSurfaces || tabBridge === null
|
|
202
|
+
? () => { }
|
|
203
|
+
: watchSharedSettings(settings);
|
|
204
|
+
const unsubscribeAccent = () => { unwatchSharedSettings(); unsubscribeAccentChange(); };
|
|
198
205
|
// Rationale: Routes open only after shell construction.
|
|
199
206
|
const references = {
|
|
200
207
|
changelog: async (input) => {
|
|
@@ -210,7 +217,9 @@ export async function composeOwnedUi(options = {}) {
|
|
|
210
217
|
},
|
|
211
218
|
session: () => import("./session-info-reference.js").then(m => m.loadSessionInfoReference(backend)),
|
|
212
219
|
};
|
|
213
|
-
const routeHost = settings === null || !ownedSurfaces ? null : createOwnedRouteHost(settings, references
|
|
220
|
+
const routeHost = settings === null || !ownedSurfaces ? null : createOwnedRouteHost(settings, references, {
|
|
221
|
+
settingsPinned: tabBridge?.tabId.startsWith("settings-") === true,
|
|
222
|
+
});
|
|
214
223
|
const viewportSettings = settings === null || !ownedSurfaces ? null : {
|
|
215
224
|
snapshot: () => viewportSettingsSnapshot(settings),
|
|
216
225
|
onChange: listener => settings.onChange(() => listener(viewportSettingsSnapshot(settings))),
|
|
@@ -436,15 +445,27 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
|
|
|
436
445
|
tabBridge.status("starting");
|
|
437
446
|
const currentSessionFile = () => currentBackendSessionFile(backend);
|
|
438
447
|
let pendingAutoName = null;
|
|
448
|
+
// Invariant: while a generated name is being applied, the session name it produces is automatic
|
|
449
|
+
// even if the session normalizes it; reporting it as `user` would block the strip's title
|
|
450
|
+
// animation and every later automatic rename.
|
|
451
|
+
let applyingAutoName = false;
|
|
439
452
|
const isDefaultName = (name) => name == null || /^agent(?: \d+)?$/i.test(name);
|
|
440
453
|
const currentSessionName = () => backend.session.view().status.footer?.sessionName ?? null;
|
|
441
454
|
const announceSession = () => {
|
|
442
455
|
const name = currentSessionName();
|
|
443
|
-
|
|
456
|
+
// Invariant: the session may announce the applied name after the workflow resolves; the
|
|
457
|
+
// first non-default name announced for a pending auto-name is that automatic name.
|
|
458
|
+
if (applyingAutoName && !isDefaultName(name)) {
|
|
459
|
+
pendingAutoName = name;
|
|
460
|
+
applyingAutoName = false;
|
|
461
|
+
}
|
|
462
|
+
const automatic = name === pendingAutoName;
|
|
463
|
+
const nameSource = automatic ? "auto" : isDefaultName(name) ? "default" : "user";
|
|
444
464
|
tabBridge.session(currentSessionFile(), name, nameSource);
|
|
445
465
|
};
|
|
446
466
|
let working = false;
|
|
447
|
-
|
|
467
|
+
// Invariant: the last turn's settlement stays reported until the next turn starts.
|
|
468
|
+
let lastTurn;
|
|
448
469
|
let autoNameAttempted = false;
|
|
449
470
|
let autoNameArmed = false;
|
|
450
471
|
// Invariant: naming launches once per tab during an agent run, as soon as its prompt is projected,
|
|
@@ -461,12 +482,26 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
|
|
|
461
482
|
if (!isDefaultName(currentSessionName()))
|
|
462
483
|
return;
|
|
463
484
|
pendingAutoName = name;
|
|
485
|
+
applyingAutoName = true;
|
|
464
486
|
const result = await backend.workflows.executeWorkflow({ command: "name", argument: name });
|
|
465
|
-
if (result.outcome
|
|
487
|
+
if (result.outcome !== "completed") {
|
|
488
|
+
applyingAutoName = false;
|
|
489
|
+
if (pendingAutoName === name)
|
|
490
|
+
pendingAutoName = null;
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const current = currentSessionName();
|
|
494
|
+
if (current !== null && !isDefaultName(current)) {
|
|
495
|
+
// Invariant: The session already holds the applied name; report it as automatic now.
|
|
496
|
+
pendingAutoName = current;
|
|
497
|
+
applyingAutoName = false;
|
|
498
|
+
tabBridge.session(currentSessionFile(), current, "auto");
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
// Invariant: The session announces later; show the generated name meanwhile, still automatic.
|
|
466
502
|
tabBridge.session(currentSessionFile(), name, "auto");
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}).catch(() => { pendingAutoName = null; });
|
|
503
|
+
}
|
|
504
|
+
}).catch(() => { pendingAutoName = null; applyingAutoName = false; });
|
|
470
505
|
};
|
|
471
506
|
const reportStatus = (lifecycle) => {
|
|
472
507
|
const view = backend.session.view();
|
|
@@ -476,7 +511,7 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
|
|
|
476
511
|
lifecycle: lifecycle ?? view.lifecycle,
|
|
477
512
|
pendingRequests,
|
|
478
513
|
queuedInput,
|
|
479
|
-
...(
|
|
514
|
+
...(lastTurn === undefined ? {} : { lastTurn }),
|
|
480
515
|
}), false, pendingRequests, queuedInput);
|
|
481
516
|
};
|
|
482
517
|
announceSession();
|
|
@@ -501,7 +536,7 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
|
|
|
501
536
|
const wasWorking = working;
|
|
502
537
|
working = event.lifecycle === "busy";
|
|
503
538
|
if (working)
|
|
504
|
-
|
|
539
|
+
lastTurn = undefined;
|
|
505
540
|
reportStatus(event.lifecycle);
|
|
506
541
|
if (wasWorking && !working && recovery !== null) {
|
|
507
542
|
void recovery.settled(currentSessionFile()).catch(() => tabBridge.degraded(true));
|
|
@@ -517,7 +552,7 @@ export async function bindResidentTabBridge(tabBridge, backend, recovery = null,
|
|
|
517
552
|
if (event.type === "agent-run-settled") {
|
|
518
553
|
launchAutoName();
|
|
519
554
|
autoNameArmed = false;
|
|
520
|
-
|
|
555
|
+
lastTurn = event.aborted ? "aborted" : event.successful ? "done" : "error";
|
|
521
556
|
tabBridge.prompt(event.aborted);
|
|
522
557
|
reportStatus();
|
|
523
558
|
}
|
|
@@ -546,6 +581,21 @@ function currentBackendSessionFile(backend) {
|
|
|
546
581
|
const resume = backend.identity.currentSessionResumeMetadata();
|
|
547
582
|
return resume === null ? null : `${resume.sessionDir}/${resume.sessionId}`;
|
|
548
583
|
}
|
|
584
|
+
const SHARED_SETTINGS_POLL_MS = 250;
|
|
585
|
+
/** Rereads the shared settings document whenever its stat changes; returns the stop. */
|
|
586
|
+
export function watchSharedSettings(settings) {
|
|
587
|
+
const file = settings.file;
|
|
588
|
+
const listener = () => {
|
|
589
|
+
try {
|
|
590
|
+
settings.refresh();
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
// Compatibility: an unreadable peer write resolves on the next stat change.
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
watchFile(file, { interval: SHARED_SETTINGS_POLL_MS, persistent: false }, listener);
|
|
597
|
+
return () => unwatchFile(file, listener);
|
|
598
|
+
}
|
|
549
599
|
async function readPackageVersion() {
|
|
550
600
|
const manifest = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
|
|
551
601
|
const version = manifest?.version;
|
|
@@ -9,5 +9,10 @@ export interface OwnedReferenceProviders {
|
|
|
9
9
|
hotkeys(): Promise<ReferenceDocumentProvider>;
|
|
10
10
|
session(): Promise<ReferenceDocumentProvider>;
|
|
11
11
|
}
|
|
12
|
+
/** How the settings route is presented. */
|
|
13
|
+
export interface OwnedRouteHostOptions {
|
|
14
|
+
/** The resident Settings tab: the screen is the tab, so it offers no screen-level close. */
|
|
15
|
+
readonly settingsPinned?: boolean;
|
|
16
|
+
}
|
|
12
17
|
/** Declares settings and lazy document routes. */
|
|
13
|
-
export declare function createOwnedRouteHost(settings: OwnedSettingsManager, references?: OwnedReferenceProviders): UiRouteHost;
|
|
18
|
+
export declare function createOwnedRouteHost(settings: OwnedSettingsManager, references?: OwnedReferenceProviders, options?: OwnedRouteHostOptions): UiRouteHost;
|
|
@@ -3,7 +3,7 @@ import { SETTINGS_APP_ID, SETTINGS_ROUTE } from "../features/owned-ui/settings-r
|
|
|
3
3
|
import { piTheme, renderPiAccentPreview } from "../integrations/pi/components/upstream/theme/theme.js";
|
|
4
4
|
import { faint } from "../ui/components/text.js";
|
|
5
5
|
/** Declares settings and lazy document routes. */
|
|
6
|
-
export function createOwnedRouteHost(settings, references) {
|
|
6
|
+
export function createOwnedRouteHost(settings, references, options = {}) {
|
|
7
7
|
const routes = new Set([SETTINGS_ROUTE, ...(references === undefined ? [] : [CHANGELOG_ROUTE, HOTKEYS_ROUTE, "session"])]);
|
|
8
8
|
return {
|
|
9
9
|
claims: route => routes.has(route),
|
|
@@ -11,7 +11,7 @@ export function createOwnedRouteHost(settings, references) {
|
|
|
11
11
|
if (!routes.has(route))
|
|
12
12
|
return null;
|
|
13
13
|
if (route === SETTINGS_ROUTE) {
|
|
14
|
-
return deferredSurface(SETTINGS_APP_ID, "settings", () => loadSettingsSurface(settings));
|
|
14
|
+
return deferredSurface(SETTINGS_APP_ID, "settings", () => loadSettingsSurface(settings, options.settingsPinned === true));
|
|
15
15
|
}
|
|
16
16
|
const changelog = route === CHANGELOG_ROUTE;
|
|
17
17
|
const session = route === "session";
|
|
@@ -61,9 +61,10 @@ function deferredSurface(id, label, load) {
|
|
|
61
61
|
onExitRequested: listener => { onExit = listener; },
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
async function loadSettingsSurface(settings) {
|
|
64
|
+
async function loadSettingsSurface(settings, pinned) {
|
|
65
65
|
const { SettingsApp } = await import("../features/owned-ui/settings-app.js");
|
|
66
|
-
|
|
66
|
+
// Invariant: a pinned Settings tab closes as a tab; neither Esc nor one Ctrl+C dismisses its screen.
|
|
67
|
+
return hostApp(SETTINGS_APP_ID, SETTINGS_ROUTE, () => new SettingsApp(settings, { pinned }), !pinned);
|
|
67
68
|
}
|
|
68
69
|
async function loadReferenceSurface(settings, id, route, title, document) {
|
|
69
70
|
// Rationale: the app module and the document load together, so a document that fails
|
|
@@ -80,7 +81,7 @@ async function loadReferenceSurface(settings, id, route, title, document) {
|
|
|
80
81
|
}),
|
|
81
82
|
}));
|
|
82
83
|
}
|
|
83
|
-
async function hostApp(id, route, create) {
|
|
84
|
+
async function hostApp(id, route, create, closeOnInterrupt = true) {
|
|
84
85
|
const [{ UiAppHost }, { UiAppRegistry }] = await Promise.all([
|
|
85
86
|
import("../ui/apps/host.js"),
|
|
86
87
|
import("../ui/apps/registry.js"),
|
|
@@ -94,7 +95,7 @@ async function hostApp(id, route, create) {
|
|
|
94
95
|
let onExit = () => undefined;
|
|
95
96
|
const host = new UiAppHost({
|
|
96
97
|
registry,
|
|
97
|
-
closeOnInterrupt
|
|
98
|
+
closeOnInterrupt,
|
|
98
99
|
theme: pinnedTheme(),
|
|
99
100
|
surface: {
|
|
100
101
|
size: () => size,
|
|
@@ -12,7 +12,10 @@ export declare const SETTINGS_SHORTCUTS: ShortcutRegistry<Action>;
|
|
|
12
12
|
export declare class SettingsApp implements UiApp {
|
|
13
13
|
#private;
|
|
14
14
|
readonly id = "settings";
|
|
15
|
-
|
|
15
|
+
/** `pinned` presents the screen as a resident Settings tab, which offers no screen-level close. */
|
|
16
|
+
constructor(session: OwnedSettingsManager, options?: {
|
|
17
|
+
readonly pinned?: boolean;
|
|
18
|
+
});
|
|
16
19
|
onActivate(host: AppHostServices): void;
|
|
17
20
|
onClose(_host: AppHostServices): void;
|
|
18
21
|
render(rect: PaneRect, host: AppHostServices): readonly string[];
|
|
@@ -120,8 +120,11 @@ export class SettingsApp {
|
|
|
120
120
|
#activeUntil = 0;
|
|
121
121
|
#activityTimer;
|
|
122
122
|
#renderedScroll;
|
|
123
|
-
|
|
123
|
+
#pinned;
|
|
124
|
+
/** `pinned` presents the screen as a resident Settings tab, which offers no screen-level close. */
|
|
125
|
+
constructor(session, options = {}) {
|
|
124
126
|
this.#session = session;
|
|
127
|
+
this.#pinned = options.pinned === true;
|
|
125
128
|
}
|
|
126
129
|
onActivate(host) {
|
|
127
130
|
this.#closed = false;
|
|
@@ -251,7 +254,8 @@ export class SettingsApp {
|
|
|
251
254
|
const selected = indexOfKey(rows, this.#selectedKey);
|
|
252
255
|
switch (action) {
|
|
253
256
|
case "close":
|
|
254
|
-
|
|
257
|
+
if (!this.#pinned)
|
|
258
|
+
host.close();
|
|
255
259
|
return { consumed: true };
|
|
256
260
|
case "open-filter":
|
|
257
261
|
this.#scrollBeforeFilter = this.#scroll;
|
|
@@ -1016,7 +1020,9 @@ export class SettingsApp {
|
|
|
1016
1020
|
return this.#dialogLines(open, width, theme);
|
|
1017
1021
|
// Rationale: standing guidance is more useful than transient feedback; failures still
|
|
1018
1022
|
// roll back without displacing the Settings interaction contract.
|
|
1019
|
-
const
|
|
1023
|
+
const entries = SETTINGS_SHORTCUTS.hintEntries(SCOPE).filter(entry => !this.#pinned
|
|
1024
|
+
|| entry.key !== DIALOG_CLOSE_SHORTCUT_HINT.key || entry.action !== DIALOG_CLOSE_SHORTCUT_HINT.action);
|
|
1025
|
+
const status = renderShortcutHintsWithClose(entries, theme, width, width > 0 ? 1 : 0);
|
|
1020
1026
|
const input = this.#filter;
|
|
1021
1027
|
if (input === null)
|
|
1022
1028
|
return [status];
|
|
@@ -13,7 +13,7 @@ export const RESIDENT_TAB_SHORTCUTS = Object.freeze([
|
|
|
13
13
|
{ id: "tabs.new", defaultKey: "alt+a", scope: "global", description: "New A1 tab" },
|
|
14
14
|
{ id: "tabs.close", defaultKey: "alt+w", scope: "global", description: "Close A1 tab" },
|
|
15
15
|
{ id: "tabs.rename", defaultKey: "f2", scope: "global", description: "Rename A1 tab" },
|
|
16
|
-
{ id: "tabs.settings", defaultKey: "alt
|
|
16
|
+
{ id: "tabs.settings", defaultKey: "alt+,", scope: "global", description: "Open Settings tab" },
|
|
17
17
|
...jumps,
|
|
18
18
|
{ id: "tabs.next", defaultKey: null, scope: "global", description: "View next A1 tab" },
|
|
19
19
|
{ id: "tabs.previous", defaultKey: null, scope: "global", description: "View previous A1 tab" },
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/** How the tab's last agent turn settled; it persists until the next turn starts. */
|
|
2
|
+
export type ResidentTabTurnSettlement = "done" | "aborted" | "error";
|
|
3
|
+
export type ResidentTabDerivedStatus = "starting" | "working" | "needs-input" | "idle" | "failed" | ResidentTabTurnSettlement;
|
|
2
4
|
export interface ResidentTabFacts {
|
|
3
5
|
readonly lifecycle: string;
|
|
4
6
|
readonly pendingRequests: number;
|
|
5
7
|
readonly queuedInput: number;
|
|
6
|
-
readonly
|
|
7
|
-
readonly settledUnseen?: boolean;
|
|
8
|
+
readonly lastTurn?: ResidentTabTurnSettlement;
|
|
8
9
|
}
|
|
9
10
|
export declare function deriveResidentTabStatus(facts: ResidentTabFacts): ResidentTabDerivedStatus;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Invariant: `failed` is reserved for a failed tab lifecycle; a turn that errored reports `error`.
|
|
1
2
|
export function deriveResidentTabStatus(facts) {
|
|
2
3
|
if (facts.lifecycle === "failed")
|
|
3
4
|
return "failed";
|
|
@@ -7,9 +8,5 @@ export function deriveResidentTabStatus(facts) {
|
|
|
7
8
|
return "working";
|
|
8
9
|
if (facts.lifecycle === "starting")
|
|
9
10
|
return "starting";
|
|
10
|
-
|
|
11
|
-
return "failed";
|
|
12
|
-
if (facts.settledUnseen === true)
|
|
13
|
-
return "done-unseen";
|
|
14
|
-
return "idle";
|
|
11
|
+
return facts.lastTurn ?? "idle";
|
|
15
12
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { RESIDENT_DETACH_INTERVAL_MS, restoreAfterOwnedExit } from "../terminal-cleanup/index.js";
|
|
3
3
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { connect } from "node:net";
|
|
5
5
|
import { resolve } from "node:path";
|
|
@@ -64,7 +64,7 @@ async function runBootstrapAttempt(options, retriesRemaining) {
|
|
|
64
64
|
residentRootExecutable: resident.executable.executable,
|
|
65
65
|
residentRootArguments: JSON.stringify([
|
|
66
66
|
resident.executable.entry,
|
|
67
|
-
"attach", "--endpoint", resident.endpoint, "--cwd", process.cwd(), "--clear-exit-ms", String(
|
|
67
|
+
"attach", "--endpoint", resident.endpoint, "--cwd", process.cwd(), "--clear-exit-ms", String(RESIDENT_DETACH_INTERVAL_MS),
|
|
68
68
|
"--profile", environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir,
|
|
69
69
|
"--settings-file", resolve(agentProfileDir, "settings.json"),
|
|
70
70
|
"--data-dir", paths.dataDir, "--runtime-dir", paths.runtimeDir,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ResidentTabStatus = "starting" | "working" | "needs-input" | "done-unseen" | "idle" | "unresponsive" | "stalled" | "crashed" | "restarting" | "failed";
|
|
1
|
+
export type ResidentTabStatus = "starting" | "working" | "needs-input" | "done-unseen" | "done" | "aborted" | "error" | "idle" | "unresponsive" | "stalled" | "crashed" | "restarting" | "failed";
|
|
2
2
|
export interface TabBridgeCredentials {
|
|
3
3
|
readonly tabId: string;
|
|
4
4
|
readonly token: string;
|
|
@@ -137,7 +137,8 @@ export class TabBridgeClient {
|
|
|
137
137
|
if (start === -1) {
|
|
138
138
|
let retained = 0;
|
|
139
139
|
const maximum = Math.min(prefix.length - 1, pending.length);
|
|
140
|
-
|
|
140
|
+
// Invariant: a lone trailing Esc is a user key and passes at once; only `\x1bP` or more is withheld.
|
|
141
|
+
for (let length = maximum; length > 1; length--) {
|
|
141
142
|
if (pending.endsWith(prefix.slice(0, length))) {
|
|
142
143
|
retained = length;
|
|
143
144
|
break;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { CLEAR_EXIT_INTERVAL_MS, EMERGENCY_TERMINAL_RESET, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
|
|
1
|
+
export { CLEAR_EXIT_INTERVAL_MS, EMERGENCY_TERMINAL_RESET, RESIDENT_DETACH_INTERVAL_MS, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
|
|
2
2
|
export { installFatalExit, writeFatalDiagnostic } from "./fatal-exit.js";
|
|
3
3
|
export type { FatalExitOptions } from "./fatal-exit.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { CLEAR_EXIT_INTERVAL_MS, EMERGENCY_TERMINAL_RESET, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
|
|
1
|
+
export { CLEAR_EXIT_INTERVAL_MS, EMERGENCY_TERMINAL_RESET, RESIDENT_DETACH_INTERVAL_MS, boundedCleanup, restoreAfterOwnedExit, restoreProcessTerminal } from "./terminal-reset.js";
|
|
2
2
|
export { installFatalExit, writeFatalDiagnostic } from "./fatal-exit.js";
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export declare const CLEAR_EXIT_INTERVAL_MS = 500;
|
|
2
|
+
/**
|
|
3
|
+
* How long a first `Ctrl+C` keeps the resident attach client's detach chord armed. It matches the
|
|
4
|
+
* owned dialogs' interrupt chord so an unhurried double press leaves resident tabs from any tab.
|
|
5
|
+
*/
|
|
6
|
+
export declare const RESIDENT_DETACH_INTERVAL_MS = 1500;
|
|
2
7
|
export declare const EMERGENCY_TERMINAL_RESET: string;
|
|
3
8
|
export declare function restoreProcessTerminal(): void;
|
|
4
9
|
export declare function restoreAfterOwnedExit(ownsTerminal: boolean, code: number | null, signal: string | null, restore?: () => void): number;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { writeSync } from "node:fs";
|
|
2
2
|
export const CLEAR_EXIT_INTERVAL_MS = 500;
|
|
3
|
+
/**
|
|
4
|
+
* How long a first `Ctrl+C` keeps the resident attach client's detach chord armed. It matches the
|
|
5
|
+
* owned dialogs' interrupt chord so an unhurried double press leaves resident tabs from any tab.
|
|
6
|
+
*/
|
|
7
|
+
export const RESIDENT_DETACH_INTERVAL_MS = 1_500;
|
|
3
8
|
// Protocol: reset modes on both screens; setting keyboard flags to zero is idempotent,
|
|
4
9
|
// unlike repeatedly popping a keyboard-protocol stack owned by a parent application.
|
|
5
10
|
// Resetting parent-screen margins homes the cursor, so preserve its restored position.
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type ProgramStatus, type WheelScrollLines } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { PiTuiComponentPort, PiTuiInputListener, PiTuiOverlayHandle, PiTuiOverlayOptions, PiTuiPreInputListener, PiTuiRuntimeAdapterOptions, PiTuiRuntimeState, PiTuiScrollState, PiTuiStopOptions, PiTuiViewport } from "./contracts.js";
|
|
3
|
+
/**
|
|
4
|
+
* Re-encodes one key report decoded by pi-tui in the legacy form, so a kitty-keyboard child
|
|
5
|
+
* (resident tabs) and a legacy terminal produce the same owned-screen action. Releases decode
|
|
6
|
+
* to nothing; mouse reports, paste, and unknown sequences pass through unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export declare function legacyKeyInput(data: string): string;
|
|
3
9
|
export type PiTuiRuntimeErrorStage = "construction" | "start" | "input-drain" | "restoration";
|
|
4
10
|
/** Identifies the Pi TUI lifecycle stage that failed while preserving the original cause. */
|
|
5
11
|
export declare class PiTuiRuntimeError extends Error {
|
|
@@ -1,8 +1,35 @@
|
|
|
1
|
-
import { HStack, ProcessTerminal, ScrollView, TuiAltScreen, TuiMainScreen, VStack, visibleWidth, } from "@earendil-works/pi-tui";
|
|
1
|
+
import { HStack, ProcessTerminal, ScrollView, TuiAltScreen, TuiMainScreen, VStack, decodeKittyPrintable, isKeyRelease, parseKey, visibleWidth, } from "@earendil-works/pi-tui";
|
|
2
2
|
import { MouseReportInput, stripSgrMouseReports } from "./mouse-report-input.js";
|
|
3
3
|
import { GeometryObservedAltScreen, OverlayGeometryTracker } from "./overlay-geometry.js";
|
|
4
4
|
import { boundedCleanup, EMERGENCY_TERMINAL_RESET } from "../../../foundation/terminal-cleanup/terminal-reset.js";
|
|
5
5
|
import { InputPresentationCoordinator, } from "./input-presentation-coordinator.js";
|
|
6
|
+
// Protocol: the legacy bytes owned screens and their key tables compare against.
|
|
7
|
+
const LEGACY_KEY_BYTES = {
|
|
8
|
+
escape: "\u001b", up: "\u001b[A", down: "\u001b[B", right: "\u001b[C", left: "\u001b[D",
|
|
9
|
+
"shift+up": "\u001b[1;2A", "shift+down": "\u001b[1;2B", pageUp: "\u001b[5~", pageDown: "\u001b[6~",
|
|
10
|
+
home: "\u001b[H", end: "\u001b[F", "ctrl+home": "\u001b[1;5H", "ctrl+end": "\u001b[1;5F",
|
|
11
|
+
enter: "\r", tab: "\t", "shift+tab": "\u001b[Z", backspace: "\u007f", delete: "\u001b[3~",
|
|
12
|
+
space: " ", "ctrl+c": "\u0003", "ctrl+z": "\u001a",
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Re-encodes one key report decoded by pi-tui in the legacy form, so a kitty-keyboard child
|
|
16
|
+
* (resident tabs) and a legacy terminal produce the same owned-screen action. Releases decode
|
|
17
|
+
* to nothing; mouse reports, paste, and unknown sequences pass through unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function legacyKeyInput(data) {
|
|
20
|
+
if (data.length <= 1 || !data.startsWith("\u001b"))
|
|
21
|
+
return data;
|
|
22
|
+
const key = parseKey(data);
|
|
23
|
+
if (key === undefined)
|
|
24
|
+
return data;
|
|
25
|
+
if (isKeyRelease(data))
|
|
26
|
+
return "";
|
|
27
|
+
const legacy = LEGACY_KEY_BYTES[key];
|
|
28
|
+
if (legacy !== undefined)
|
|
29
|
+
return legacy;
|
|
30
|
+
const printable = decodeKittyPrintable(data);
|
|
31
|
+
return printable !== undefined && printable >= " " && printable !== "\u007f" ? printable : data;
|
|
32
|
+
}
|
|
6
33
|
/** Identifies the Pi TUI lifecycle stage that failed while preserving the original cause. */
|
|
7
34
|
export class PiTuiRuntimeError extends Error {
|
|
8
35
|
stage;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"architecture": "arm64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-19T14:46:40.019Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
13
|
"sha256": "a87eaaffcd986b624c8b044b756446cc1bdaf4334a1775b97820542fe34f9fb7",
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"architecture": "arm64",
|
|
10
10
|
"target": "aarch64-apple-darwin",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-10-
|
|
12
|
+
"builtAt": "2026-10-19T14:50:21.966Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "12b73c5b9c2023badcdcbea9f2c3cca018cbb1704c0cb452303f59a551b0c407",
|
|
16
|
+
"size": 5813520
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "darwin",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-19T14:46:46.372Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
13
|
"sha256": "783a65f0de287a1c02cd5429dbe97bc8633f4cc8a7b92e3e961180b9bdcff814",
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"architecture": "x64",
|
|
10
10
|
"target": "x86_64-apple-darwin",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-10-
|
|
12
|
+
"builtAt": "2026-10-19T14:53:04.323Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "aea613cb66de9d7f87018ee32aba7d30d695a86752301e1dde771bdeb7dad724",
|
|
16
|
+
"size": 6349264
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"platform": "linux",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-19T14:46:32.168Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
13
|
"sha256": "118e5954e3864672bd8482ab7edf419cb27dcdf2533720316979b7f5cc5a53a5",
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"architecture": "x64",
|
|
10
10
|
"target": "x86_64-unknown-linux-gnu",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-10-
|
|
12
|
+
"builtAt": "2026-10-19T14:49:27.044Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "890d87b2fccecb4956243c40da97c461e180a40ace6b340d5c9f808ab9f4b586",
|
|
16
|
+
"size": 7099552
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
"platform": "win32",
|
|
8
8
|
"architecture": "x64",
|
|
9
9
|
"capability": "supported",
|
|
10
|
-
"builtAt": "2026-10-
|
|
10
|
+
"builtAt": "2026-10-19T14:46:55.773Z",
|
|
11
11
|
"artifact": {
|
|
12
12
|
"filename": "process-guardian.node",
|
|
13
|
-
"sha256": "
|
|
13
|
+
"sha256": "e8b39eb8c589c028024164c021a08183021574d06b48fbefc34fb3fb55636c62",
|
|
14
14
|
"size": 402432
|
|
15
15
|
},
|
|
16
16
|
"provenance": {
|
|
Binary file
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"architecture": "x64",
|
|
10
10
|
"target": "x86_64-pc-windows-msvc",
|
|
11
11
|
"capability": "supported",
|
|
12
|
-
"builtAt": "2026-10-
|
|
12
|
+
"builtAt": "2026-10-19T14:49:14.794Z",
|
|
13
13
|
"artifact": {
|
|
14
14
|
"filename": "terminal-host.node",
|
|
15
|
-
"sha256": "
|
|
16
|
-
"size":
|
|
15
|
+
"sha256": "7661bc29672ef40853a94bcfe367fd6ed5c03b9e6e3e3ac4ac4bbea4c2aad891",
|
|
16
|
+
"size": 6323712
|
|
17
17
|
},
|
|
18
18
|
"entry": {
|
|
19
19
|
"filename": "dist/foundation/resident-tabs/host-entry.js",
|
|
Binary file
|
|
@@ -47,6 +47,12 @@ export declare class OwnedSettingsManager {
|
|
|
47
47
|
/** Reads and resolves the product namespace. Never throws: every failure becomes a notice. */
|
|
48
48
|
read(): OwnedUiSettingsResolution;
|
|
49
49
|
load(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Adopts values another process stored in the shared document. Notifies only when a
|
|
52
|
+
* resolved value differs, so a peer's own write echo stays silent. Restart-bound values
|
|
53
|
+
* keep their session-start effect and surface as pending instead.
|
|
54
|
+
*/
|
|
55
|
+
refresh(): boolean;
|
|
50
56
|
sections(): readonly OwnedUiSettingsSection[];
|
|
51
57
|
/**
|
|
52
58
|
* The value in effect for a declared setting, typed by its declaration. A restart-bound
|
|
@@ -129,6 +129,28 @@ export class OwnedSettingsManager {
|
|
|
129
129
|
this.#agentSnapshot = agent === null ? null : await snapshotOf(agent, this.#hiddenAgentSettingIds);
|
|
130
130
|
this.#notify();
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Adopts values another process stored in the shared document. Notifies only when a
|
|
134
|
+
* resolved value differs, so a peer's own write echo stays silent. Restart-bound values
|
|
135
|
+
* keep their session-start effect and surface as pending instead.
|
|
136
|
+
*/
|
|
137
|
+
refresh() {
|
|
138
|
+
const next = this.read();
|
|
139
|
+
if (resolvedValuesKey(next) === resolvedValuesKey(this.#resolution))
|
|
140
|
+
return false;
|
|
141
|
+
this.#resolution = next;
|
|
142
|
+
for (const setting of next.settings) {
|
|
143
|
+
const id = setting.declaration.id;
|
|
144
|
+
if (!this.#restartEffective.has(id))
|
|
145
|
+
continue;
|
|
146
|
+
if (setting.value === this.#restartEffective.get(id))
|
|
147
|
+
this.#pending.delete(id);
|
|
148
|
+
else
|
|
149
|
+
this.#pending.set(id, setting.value);
|
|
150
|
+
}
|
|
151
|
+
this.#notify();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
132
154
|
sections() {
|
|
133
155
|
const resolution = this.#hiddenOwnedSettingIds.size === 0
|
|
134
156
|
? this.#resolution
|
|
@@ -270,6 +292,9 @@ async function snapshotOf(agent, hidden) {
|
|
|
270
292
|
return { descriptors: [], writeAdvertised: false, failure: describe(error) };
|
|
271
293
|
}
|
|
272
294
|
}
|
|
295
|
+
function resolvedValuesKey(resolution) {
|
|
296
|
+
return JSON.stringify(resolution.settings.map(setting => [setting.declaration.id, setting.value]));
|
|
297
|
+
}
|
|
273
298
|
function failed(failure) {
|
|
274
299
|
return {
|
|
275
300
|
status: "failed",
|
|
@@ -262,11 +262,12 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
262
262
|
| `idle` | settled idle | none |
|
|
263
263
|
| `working` | agent or turn start, tool execution, compaction | shared progress frames |
|
|
264
264
|
| `needs-input` | an extension UI, trust, or permission request is pending | yellow `?` (warning) |
|
|
265
|
-
| `done
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
265
|
+
| `done` | the last turn finished successfully | green `✓` success |
|
|
266
|
+
| `aborted` | the last turn was aborted | red `✗` error |
|
|
267
|
+
| `error` | the last turn ended with an error | red `✗` error |
|
|
268
|
+
| `crashed` / `restarting` / `failed` | holder or child exit, or a failed tab lifecycle (never a failed turn) | `✗` error plus a banner |
|
|
268
269
|
|
|
269
|
-
- `
|
|
270
|
+
- A `done`, `aborted`, or `error` marker stays on the chip, including after any client views the tab, until that tab's next turn starts.
|
|
270
271
|
- A tab with a broken bridge shows only process-level states.
|
|
271
272
|
- Future CLI tabs will derive status from process state, and may use BEL or OSC 9/777 from the program as an attention input.
|
|
272
273
|
- The glyph set is the same in every client. A working spinner uses the resolved A1 accent reported by that specific tab.
|
|
@@ -335,7 +336,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
335
336
|
| `Alt+A` | new tab |
|
|
336
337
|
| `Alt+W` | close |
|
|
337
338
|
| `F2` | rename |
|
|
338
|
-
| `Alt
|
|
339
|
+
| `Alt+,` | open the global Settings tab, or close it while it is viewed |
|
|
339
340
|
| `Alt+1`…`Alt+9`, `Alt+0` | jump to a tab |
|
|
340
341
|
| unbound (`tabs.next` / `tabs.previous`) | next / previous, wrapping |
|
|
341
342
|
| unbound (`tabs.moveRight` / `tabs.moveLeft`) | move the tab right / left |
|
|
@@ -349,7 +350,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
349
350
|
- right-click opens `Rename`/`Close` (`Close` only for Settings);
|
|
350
351
|
- dragging reorders, with a `│` drop marker, after 250 ms or once the pointer moves;
|
|
351
352
|
- `+` opens a persistent dark v2 menu with `+ Agent`, a separator, and `Settings`; hover and click remain active through the opening release.
|
|
352
|
-
- **Settings.** The strip owns one profile-global `Settings` tab. The grouped launcher
|
|
353
|
+
- **Settings.** The strip owns one profile-global `Settings` tab. The grouped launcher creates or activates it; `Alt+,` does the same from another tab and closes it while it is viewed. Esc does not close the Settings tab; closing the tab removes it. Resident Agent autocomplete omits `/settings`, and manually entering it does not open a per-Agent screen. `a1 solo` and `a1 pi` retain their existing Settings routes.
|
|
353
354
|
- **Commands inside A1 tabs:** existing `/name <name>` also renames the tab. Milestone 5 adds no tab-management slash commands: `/new-tab`, `/close`, `/tabs`, `/quit-all`, and `/restart-tab` are not exposed.
|
|
354
355
|
- **Naming:**
|
|
355
356
|
- Default names are `agent` or `agent N`.
|
package/package.json
CHANGED