Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,25 @@ playwright-report/
test-results/

.worktrees/
native/

# native/ holds the macOS Tauri app and the iOS app, and both are tracked. A
# blanket `native/` ignore made git skip them during a worktree scan, so every
# commit touching them needed `git add -f` and `git add -A` silently missed
# them entirely. Ignore the build output instead.
native/**/target/
native/ios/.build/
native/ios/.derivedData/
native/macos/psyche-build-tauri/src-tauri/gen/schemas/
**/xcuserdata/
**/DerivedData/

# The tauri app is a pnpm workspace member, so the root pnpm-lock.yaml is the
# one pnpm reads and the one that pins its dependencies. A lockfile inside the
# member is a leftover from running `pnpm install` in that directory: nothing
# consumes it, and tracking it would give a second, silently drifting answer to
# "which versions does the desktop app use?".
native/macos/psyche-build-tauri/pnpm-lock.yaml

.vercel/
/.superpowers
/.opencode
Expand Down
50 changes: 20 additions & 30 deletions __tests__/tauriCovenLaunch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,7 @@ describe('native Coven launch routing', () => {
},
);
const closeThread = compileFunction<(id: string) => boolean>(functionSource('closeThread'), {
forgetThreadInSets: () => undefined,
findThread: () => thread,
detachThreadPane: () => null,
pendingDataBuffers: new Map(),
Expand Down Expand Up @@ -921,6 +922,7 @@ describe('native Coven launch routing', () => {
const state = { threads: [thread], activeThreadId: thread.id };
let stopCalls = 0;
const closeThread = compileFunction<(id: string) => boolean>(functionSource('closeThread'), {
forgetThreadInSets: () => undefined,
findThread: () => thread,
detachThreadPane: () => null,
pendingDataBuffers: new Map(),
Expand Down Expand Up @@ -995,6 +997,7 @@ describe('native Coven launch routing', () => {
},
);
const closeThread = compileFunction<(id: string) => boolean>(functionSource('closeThread'), {
forgetThreadInSets: () => undefined,
findThread: () => state.threads.find((value) => value.id === thread.id) || null,
detachThreadPane: () => null,
pendingDataBuffers: new Map(),
Expand Down Expand Up @@ -1107,37 +1110,24 @@ describe('native Coven launch routing', () => {
},
);

it('shows one retry control only for failed and exited panes', () => {
const retry = { hidden: false, setAttribute: () => undefined };
const attributes = new Map<string, string>();
const thread = {
name: 'Coven', status: 'starting', paneTitle: { textContent: '' },
paneStatus: { textContent: '' }, paneRetry: retry,
paneClose: { setAttribute: (key: string, value: string) => attributes.set(key, value) },
};
const syncThreadPaneMetadata = compileFunction<(value: typeof thread) => void>(
functionSource('syncThreadPaneMetadata'), {},
it('offers retry from the pane menu only for failed and exited panes', () => {
const source = functionSource('mountTerminal');

// The header dropped its retry button, so the pane's context menu is the
// only place the action can live - the sidebar row cannot carry it, since
// exited rows are hidden from the rail.
expect(source).not.toContain('terminal-pane-retry');
expect(source).toMatch(
/thread\.status === "exited" \|\| thread\.status === "failed"[\s\S]*label: "Retry"/
);
expect(source).toMatch(/label: "Retry"[\s\S]*retryThread\(thread\.id\)/);

// The in-flight guard that the old hidden-button logic enforced still lives
// in retryThread itself, which its own lifecycle tests cover.
expect(functionSource('retryThread')).toMatch(/thread\.startInFlight \|\| thread\.closeStarted/);
expect(functionSource('retryThread')).toMatch(
/thread\.status !== "exited" && thread\.status !== "failed"/
);
syncThreadPaneMetadata(thread);
expect(retry.hidden).toBe(true);
thread.status = 'failed';
syncThreadPaneMetadata(thread);
expect(retry.hidden).toBe(false);
thread.status = 'running';
syncThreadPaneMetadata(thread);
expect(retry.hidden).toBe(true);
thread.status = 'exited';
(thread as typeof thread & { startInFlight: boolean }).startInFlight = true;
syncThreadPaneMetadata(thread);
expect(retry.hidden).toBe(true);
(thread as typeof thread & { startInFlight: boolean }).startInFlight = false;
syncThreadPaneMetadata(thread);
expect(retry.hidden).toBe(false);
expect(attributes.get('aria-label')).toBe('Stop and close Coven');

const mount = functionSource('mountTerminal');
expect(mount.match(/className = "terminal-pane-retry"/g)).toHaveLength(1);
expect(mount).toMatch(/retry\.addEventListener\("click", function \(event\) \{[\s\S]*event\.stopPropagation\(\);[\s\S]*retryThread\(thread\.id\)/);
});

it('routes native defaults to Coven while retaining explicit shell and Psyche commands', () => {
Expand Down
Loading