Reuse terminals for non-service scripts and track lifecycle via OSC 633
Scripts launched from the UI now run in a dedicated terminal per script (titled after the script) and reuse the same terminal across re-runs. Script lifecycle is driven by a new onCommandFinished signal sourced from OSC 633 command-finished events emitted by the zsh shell integration, so foreground script exit updates status without killing the terminal. Script payloads now carry terminalId so the workspace can surface the associated tab from the scripts dropdown.
This commit is contained in:
@@ -61,6 +61,7 @@ describe("normalizeWorkspaceDescriptor", () => {
|
||||
lifecycle: "running" as const,
|
||||
health: "healthy" as const,
|
||||
exitCode: null,
|
||||
terminalId: null,
|
||||
},
|
||||
];
|
||||
const workspace = normalizeWorkspaceDescriptor({
|
||||
@@ -88,6 +89,7 @@ describe("normalizeWorkspaceDescriptor", () => {
|
||||
lifecycle: "running",
|
||||
health: "healthy",
|
||||
exitCode: null,
|
||||
terminalId: null,
|
||||
},
|
||||
]);
|
||||
expect(workspace.scripts).not.toBe(scripts);
|
||||
@@ -137,6 +139,7 @@ describe("mergeWorkspaces", () => {
|
||||
lifecycle: "running",
|
||||
health: "healthy",
|
||||
exitCode: null,
|
||||
terminalId: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -152,6 +155,7 @@ describe("mergeWorkspaces", () => {
|
||||
lifecycle: "running",
|
||||
health: "healthy",
|
||||
exitCode: null,
|
||||
terminalId: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -263,6 +267,7 @@ describe("patchWorkspaceScripts", () => {
|
||||
lifecycle: "running" as const,
|
||||
health: "healthy" as const,
|
||||
exitCode: null,
|
||||
terminalId: null,
|
||||
};
|
||||
const workspace = createWorkspace({ id: "/repo/main", scripts: [script] });
|
||||
const current = new Map([[workspace.id, workspace]]);
|
||||
|
||||
@@ -203,6 +203,7 @@ export interface WorkspaceTabSnapshot {
|
||||
terminalsHydrated: boolean;
|
||||
activeAgentIds: Iterable<string>;
|
||||
knownAgentIds: Iterable<string>;
|
||||
knownTerminalIds?: Iterable<string>;
|
||||
standaloneTerminalIds: Iterable<string>;
|
||||
hasActivePendingDraftCreate?: boolean;
|
||||
}
|
||||
@@ -1454,6 +1455,9 @@ export function reconcileWorkspaceTabs(
|
||||
const activeAgentIds = normalizeStringSet(snapshot.activeAgentIds);
|
||||
const knownAgentIds = normalizeStringSet(snapshot.knownAgentIds);
|
||||
const standaloneTerminalIds = normalizeStringSet(snapshot.standaloneTerminalIds);
|
||||
const knownTerminalIds = snapshot.knownTerminalIds
|
||||
? normalizeStringSet(snapshot.knownTerminalIds)
|
||||
: standaloneTerminalIds;
|
||||
const visibleAgentIds = new Set(activeAgentIds);
|
||||
for (const agentId of pinnedAgentIds) {
|
||||
if (knownAgentIds.has(agentId)) {
|
||||
@@ -1537,7 +1541,7 @@ export function reconcileWorkspaceTabs(
|
||||
if (
|
||||
isTerminalTab(tab) &&
|
||||
snapshot.terminalsHydrated &&
|
||||
!standaloneTerminalIds.has(tab.target.terminalId)
|
||||
!knownTerminalIds.has(tab.target.terminalId)
|
||||
) {
|
||||
nextLayout =
|
||||
closeTabInLayout({
|
||||
|
||||
@@ -1107,6 +1107,47 @@ describe("workspace-layout-store actions", () => {
|
||||
expect(useWorkspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey)).toEqual([]);
|
||||
});
|
||||
|
||||
it("reconcileTabs auto-opens only standalone terminals while keeping explicitly opened live terminals", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
|
||||
const scriptTabId = store.openTabFocused(workspaceKey, {
|
||||
kind: "terminal",
|
||||
terminalId: "term-script",
|
||||
});
|
||||
|
||||
store.reconcileTabs(workspaceKey, {
|
||||
agentsHydrated: true,
|
||||
terminalsHydrated: true,
|
||||
activeAgentIds: [],
|
||||
knownAgentIds: [],
|
||||
knownTerminalIds: ["term-script", "term-manual"],
|
||||
standaloneTerminalIds: ["term-manual"],
|
||||
hasActivePendingDraftCreate: false,
|
||||
});
|
||||
|
||||
const tabs = useWorkspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey);
|
||||
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
|
||||
expect(tabs.map((tab) => tab.tabId)).toEqual(["terminal_term-script", "terminal_term-manual"]);
|
||||
expect(findPaneById(layout.root, layout.focusedPaneId)?.focusedTabId).toBe(scriptTabId);
|
||||
});
|
||||
|
||||
it("reconcileTabs does not auto-open live non-standalone terminals", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
|
||||
useWorkspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
|
||||
agentsHydrated: true,
|
||||
terminalsHydrated: true,
|
||||
activeAgentIds: [],
|
||||
knownAgentIds: [],
|
||||
knownTerminalIds: ["term-script"],
|
||||
standaloneTerminalIds: [],
|
||||
hasActivePendingDraftCreate: false,
|
||||
});
|
||||
|
||||
expect(useWorkspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey)).toEqual([]);
|
||||
});
|
||||
|
||||
it("explicitly opening an agent tab clears hidden intent", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
|
||||
Reference in New Issue
Block a user