# Temporary Session Group Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Group operating-system temporary workspaces under a collapsed sidebar section without deleting their session history or eagerly loading their threads.

**Architecture:** Classify decoded workspace paths in the extension backend using boundary-safe comparisons against raw and canonical system temp roots, then expose `isTemporary` in the existing workspace payload. Partition workspaces in a testable browser utility and render temporary workspaces through the existing row UI only after the user expands the group.

**Tech Stack:** TypeScript extension, Node.js ESM utilities and assertion scripts, plain JavaScript WebView UI, existing English/Simplified Chinese i18n table.

---

### Task 1: Add Boundary-Safe Temporary Path Classification

**Files:**
- Create: `workspace-utils.js`
- Create: `scripts/test-workspace-utils.mjs`
- Modify: `package.json`

**Step 1: Write the failing utility test**

Create `scripts/test-workspace-utils.mjs` with native and explicit Windows cases:

```js
import assert from "node:assert/strict";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
  getSystemTemporaryRoots,
  isPathInsideRoot,
  isTemporaryWorkspacePath,
} from "../workspace-utils.js";

const nativeRoot = tmpdir();
assert.equal(isPathInsideRoot(nativeRoot, nativeRoot), true);
assert.equal(isPathInsideRoot(join(nativeRoot, "pi-123", "repo"), nativeRoot), true);
assert.equal(isPathInsideRoot(`${nativeRoot}-sibling`, nativeRoot), false);
assert.equal(isPathInsideRoot("", nativeRoot), false);
assert.equal(isPathInsideRoot(nativeRoot, ""), false);

assert.equal(
  isPathInsideRoot(
    "C:\\Users\\ADMINI~1\\AppData\\Local\\Temp\\pi-123",
    "c:\\users\\admini~1\\appdata\\local\\temp",
    "win32",
  ),
  true,
);
assert.equal(
  isPathInsideRoot(
    "C:\\Users\\ADMINI~1\\AppData\\Local\\Temporary\\pi-123",
    "C:\\Users\\ADMINI~1\\AppData\\Local\\Temp",
    "win32",
  ),
  false,
);

assert.ok(getSystemTemporaryRoots().length >= 1);
assert.equal(isTemporaryWorkspacePath(join(nativeRoot, "pi-123")), true);
assert.equal(isTemporaryWorkspacePath(process.cwd()), false);
assert.equal(
  isTemporaryWorkspacePath(
    "C:\\Users\\Administrator\\AppData\\Local\\Temp\\pi-123",
    [
      "C:\\Users\\ADMINI~1\\AppData\\Local\\Temp",
      "C:\\Users\\Administrator\\AppData\\Local\\Temp",
    ],
    "win32",
  ),
  true,
);
```

**Step 2: Run the test to verify it fails**

Run: `node scripts/test-workspace-utils.mjs`

Expected: FAIL with `ERR_MODULE_NOT_FOUND` for `workspace-utils.js`.

**Step 3: Implement the minimal path utility**

Create `workspace-utils.js`:

```js
import { realpathSync } from "node:fs";
import { tmpdir } from "node:os";
import { posix, win32 } from "node:path";

function pathApi(platform) {
  return platform === "win32" ? win32 : posix;
}

function normalizedPath(value, platform) {
  const api = pathApi(platform);
  const normalized = api.resolve(value);
  return platform === "win32" ? normalized.toLowerCase() : normalized;
}

export function isPathInsideRoot(candidate, root, platform = process.platform) {
  if (typeof candidate !== "string" || candidate.length === 0) return false;
  if (typeof root !== "string" || root.length === 0) return false;

  const api = pathApi(platform);
  const relative = api.relative(
    normalizedPath(root, platform),
    normalizedPath(candidate, platform),
  );
  return relative === ""
    || (relative !== ".." && !relative.startsWith(`..${api.sep}`) && !api.isAbsolute(relative));
}

export function getSystemTemporaryRoots() {
  const rawRoot = tmpdir();
  const roots = [rawRoot];
  try {
    roots.push(realpathSync.native(rawRoot));
  } catch {}
  return [...new Set(roots)];
}

export function isTemporaryWorkspacePath(
  workspacePath,
  temporaryRoots = getSystemTemporaryRoots(),
  platform = process.platform,
) {
  return temporaryRoots.some(root => isPathInsideRoot(workspacePath, root, platform));
}
```

Do not use substring matching; sibling paths such as `Temp-old` must remain normal.

**Step 4: Add and run the focused script**

Add to `package.json`:

```json
"verify:workspace-utils": "node scripts/test-workspace-utils.mjs"
```

Add `node --check workspace-utils.js`, `node --check scripts/test-workspace-utils.mjs`, and `npm run verify:workspace-utils` to `check`.

Run: `npm run verify:workspace-utils`

Expected: PASS with exit code 0.

**Step 5: Commit**

```bash
git add workspace-utils.js scripts/test-workspace-utils.mjs package.json
git commit -m "test: add temporary workspace path classification"
```

### Task 2: Expose Temporary Classification in Workspace Payloads

**Files:**
- Modify: `index.ts:16-25`
- Modify: `index.ts:467-508`
- Modify: `index.ts:770-782`
- Create: `scripts/verify-temporary-workspace-data.mjs`
- Modify: `package.json`

**Step 1: Write the failing integration verifier**

Create `scripts/verify-temporary-workspace-data.mjs` to read `index.ts` and assert all backend contract points:

```js
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";

const source = readFileSync("index.ts", "utf8");

assert.match(source, /from "\.\/workspace-utils\.js"/);
assert.match(source, /isTemporary:\s*boolean/);
assert.match(source, /isTemporary:\s*isTemporaryWorkspacePath\(decodedPath\)/);
assert.match(source, /workspaces:\s*Array<\{[^}]*isTemporary:\s*boolean/s);
assert.match(source, /type:\s*"workspaces-list",\s*workspaces:\s*wsList/);
```

**Step 2: Run the verifier to confirm failure**

Run: `node scripts/verify-temporary-workspace-data.mjs`

Expected: FAIL because the import and `isTemporary` contract are absent.

**Step 3: Add backend classification**

Import the utility in `index.ts`:

```ts
import { isTemporaryWorkspacePath } from "./workspace-utils.js";
```

Extend the local result type in `getWorkspaces()` and the `DesktopWindowData.workspaces` type with:

```ts
isTemporary: boolean;
```

When pushing each non-empty workspace, preserve the existing decoded path and sorting behavior:

```ts
workspaces.push({
  name,
  path: decodedPath,
  dirName,
  sessionCount,
  lastActive,
  isTemporary: isTemporaryWorkspacePath(decodedPath),
});
```

Do not remove the existing encoded-name exclusions and do not read JSONL contents during enumeration.

**Step 4: Register and run the verifier**

Add:

```json
"verify:temporary-workspace-data": "node scripts/verify-temporary-workspace-data.mjs"
```

Add its syntax check and npm invocation to `check`.

Run: `npm run verify:temporary-workspace-data`

Expected: PASS.

Run: `npx tsc --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 --skipLibCheck index.ts`

Expected: PASS.

**Step 5: Commit**

```bash
git add index.ts scripts/verify-temporary-workspace-data.mjs package.json
git commit -m "feat: classify temporary session workspaces"
```

### Task 3: Add Testable Frontend Workspace Partitioning

**Files:**
- Create: `web/temporary-workspace-utils.js`
- Create: `scripts/test-temporary-workspace-utils.mjs`
- Modify: `web/index.html`
- Modify: `index.ts:815-860`
- Modify: `package.json`

**Step 1: Write the failing browser utility test**

Create `scripts/test-temporary-workspace-utils.mjs` using the existing VM pattern:

```js
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";

const source = readFileSync("web/temporary-workspace-utils.js", "utf8");
const context = {};
runInNewContext(source, context);
const utils = context.TemporaryWorkspaceUtils;

const workspaces = [
  { dirName: "current", path: "D:/repo", sessionCount: 2 },
  { dirName: "normal", path: "D:/other", sessionCount: 3 },
  { dirName: "temp-a", path: "C:/Temp/a", sessionCount: 4, isTemporary: true },
  { dirName: "temp-hidden", path: "C:/Temp/b", sessionCount: 5, isTemporary: true },
];

const groups = utils.partitionWorkspaces(workspaces, "D:/repo", { "temp-hidden": true });
assert.deepEqual(groups.normal.map(item => item.dirName), ["normal"]);
assert.deepEqual(groups.temporary.map(item => item.dirName), ["temp-a"]);
assert.deepEqual(groups.hidden.map(item => item.dirName), ["temp-hidden"]);
assert.deepEqual(utils.summarizeTemporaryWorkspaces(groups.temporary), {
  workspaces: 1,
  sessions: 4,
});
```

**Step 2: Run the test to confirm failure**

Run: `node scripts/test-temporary-workspace-utils.mjs`

Expected: FAIL because `web/temporary-workspace-utils.js` does not exist.

**Step 3: Implement the browser utility**

Create an IIFE consistent with `web/language-toggle-utils.js`:

```js
(function installTemporaryWorkspaceUtils(global) {
  function partitionWorkspaces(workspaces, cwd, hiddenWorkspaces) {
    const groups = { normal: [], temporary: [], hidden: [] };
    for (const workspace of workspaces || []) {
      if (workspace.path === cwd) continue;
      if (hiddenWorkspaces && hiddenWorkspaces[workspace.dirName]) {
        groups.hidden.push(workspace);
      } else if (workspace.isTemporary) {
        groups.temporary.push(workspace);
      } else {
        groups.normal.push(workspace);
      }
    }
    return groups;
  }

  function summarizeTemporaryWorkspaces(workspaces) {
    return {
      workspaces: workspaces.length,
      sessions: workspaces.reduce((total, workspace) => total + (workspace.sessionCount || 0), 0),
    };
  }

  global.TemporaryWorkspaceUtils = {
    partitionWorkspaces,
    summarizeTemporaryWorkspaces,
  };
})(globalThis);
```

**Step 4: Inject the utility into runtime HTML**

Add a `__TEMPORARY_WORKSPACE_UTILS__` script placeholder to `web/index.html` before `__APP_JS__`. In `index.ts`, read `web/temporary-workspace-utils.js`, include its length in `staticSize`, and replace the placeholder when building HTML, following the existing language-toggle utility pattern.

Register `verify:temporary-workspace-utils`, syntax checks for both new files, and its invocation in `check`.

Run: `npm run verify:temporary-workspace-utils`

Expected: PASS.

Run: `npm run verify:runtime-html`

Expected: PASS after updating the runtime verifier if it enumerates required placeholders.

**Step 5: Commit**

```bash
git add web/temporary-workspace-utils.js scripts/test-temporary-workspace-utils.mjs web/index.html index.ts scripts/verify-runtime-html.mjs package.json
git commit -m "test: add temporary workspace grouping utilities"
```

### Task 4: Render the Collapsed Temporary Sessions Group

**Files:**
- Modify: `web/app.js:15-305`
- Modify: `web/app.js:390-415`
- Modify: `web/app.js:1050-1200`
- Create: `scripts/verify-temporary-session-group.mjs`
- Modify: `package.json`

**Step 1: Write the failing UI structure verifier**

Create `scripts/verify-temporary-session-group.mjs` that asserts:

```js
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";

const source = readFileSync("web/app.js", "utf8");

assert.match(source, /"workspace\.temporarySessions"/);
assert.match(source, /"workspace\.temporarySummary"/);
assert.match(source, /temporaryWorkspacesExpanded:\s*false/);
assert.match(source, /TemporaryWorkspaceUtils\.partitionWorkspaces/);
assert.match(source, /data-temporary-workspaces-toggle/);
assert.match(source, /if \(state\.temporaryWorkspacesExpanded\)/);
assert.match(source, /renderWorkspaceRows\(groups\.temporary,\s*\{\s*searchEnabled:\s*false\s*\}\)/s);
assert.doesNotMatch(source, /state\.temporaryWorkspacesExpanded\s*\|\|\s*threadSearchQuery/);
```

The last assertion protects the performance requirement: search must not force the temporary group open.

**Step 2: Run the verifier to confirm failure**

Run: `node scripts/verify-temporary-session-group.mjs`

Expected: FAIL because the translations, state, and group markup are absent.

**Step 3: Add localized labels and state**

Add English keys:

```js
"workspace.temporarySessions": "Temporary sessions",
"workspace.temporarySummary": "{workspaces} workspaces / {sessions} sessions",
```

Add Simplified Chinese keys:

```js
"workspace.temporarySessions": "\u4e34\u65f6\u4f1a\u8bdd",
"workspace.temporarySummary": "{workspaces} \u4e2a\u5de5\u4f5c\u533a / {sessions} \u4e2a\u4f1a\u8bdd",
```

Add window-local state:

```js
temporaryWorkspacesExpanded: false,
```

**Step 4: Extract reusable workspace row rendering**

Move the existing other-workspace loop into:

```js
function renderWorkspaceRows(workspaces, { searchEnabled = true } = {}) {
  let rowsHtml = "";
  for (const ws of workspaces) {
    const sessions = state.workspaceSessions[ws.dirName] || [];
    const filteredSessions = sessions.filter(session => matchesThreadSearch(session.name));
    const hasWsMatches = searchEnabled && threadSearchQuery && filteredSessions.length > 0;
    const sessionsLoading = searchEnabled && threadSearchQuery && !state.workspaceSessions[ws.dirName];

    if (sessionsLoading) send({ type: "get-workspace-sessions", dirName: ws.dirName });
    if (searchEnabled && threadSearchQuery && !hasWsMatches && !sessionsLoading) continue;

    // Append the existing workspace row and expanded session markup to rowsHtml.
    // Preserve data-ws-toggle, data-ws-launch, data-thread-idx, data-ws-file,
    // active/read-only styling, escaping, truncation, and timeAgo behavior.
  }
  return rowsHtml;
}
```

This is a mechanical extraction: do not change individual workspace markup or event data attributes.

**Step 5: Partition and render the group**

Replace the current visible/hidden filters with:

```js
const groups = TemporaryWorkspaceUtils.partitionWorkspaces(
  workspaces,
  data.cwd || "",
  state.hiddenWorkspaces,
);

html += renderWorkspaceRows(groups.normal);
```

After the existing no-results calculation and before assigning `projectTreeEl.innerHTML`, append the temporary group only when it is non-empty:

```js
if (groups.temporary.length > 0) {
  const summary = TemporaryWorkspaceUtils.summarizeTemporaryWorkspaces(groups.temporary);
  html += `
    <div class="flex items-center gap-2 px-3 py-2 text-[13px] cursor-pointer hover:bg-pi-sidebar-hover rounded-md"
         data-temporary-workspaces-toggle>
      <span class="material-symbols-outlined msym-xs text-pi-text-muted"
            style="transition:transform 0.15s;transform:rotate(${state.temporaryWorkspacesExpanded ? 90 : 0}deg);">chevron_right</span>
      <span class="material-symbols-outlined msym-xs text-pi-text-muted">schedule</span>
      <span class="min-w-0 flex-1 font-medium truncate">${t("workspace.temporarySessions")}</span>
      <span class="text-[10px] text-pi-text-dim flex-shrink-0">${t("workspace.temporarySummary", summary)}</span>
    </div>`;
  if (state.temporaryWorkspacesExpanded) {
    html += `<div data-temporary-workspaces>${renderWorkspaceRows(groups.temporary, { searchEnabled: false })}</div>`;
  }
}
```

If the full summary is too wide at the existing sidebar width, place the summary on a second truncated line within the flexible label container rather than allowing overlap. Keep the row height stable in both collapsed and expanded states.

Pass `groups.hidden` to `renderHiddenWorkspacesBar()`.

Add the group click handler after setting `innerHTML`:

```js
projectTreeEl.querySelector("[data-temporary-workspaces-toggle]")?.addEventListener("click", () => {
  state.temporaryWorkspacesExpanded = !state.temporaryWorkspacesExpanded;
  renderProjectTree();
});
```

The handler must not send `get-workspace-sessions`; only individual workspace expansion may do that.

**Step 6: Register and run focused verification**

Add `verify:temporary-session-group`, its syntax check, and its invocation to `check`.

Run: `npm run verify:temporary-session-group`

Expected: PASS.

Run: `npm run verify:i18n`

Expected: PASS with the used-key count increased by two.

Run: `npm run verify:runtime-html`

Expected: PASS.

**Step 7: Commit**

```bash
git add web/app.js scripts/verify-temporary-session-group.mjs package.json
git commit -m "feat: collapse temporary sessions in sidebar"
```

### Task 5: Document and Verify the Complete Feature

**Files:**
- Modify: `README.md`
- Test: all files touched above

**Step 1: Update user-facing documentation**

In the sidebar/workspace feature description, state that workspaces under the
operating system temporary directory are retained but grouped under a collapsed
`Temporary sessions` section. Explicitly state that expanding or collapsing the
group never deletes session history.

**Step 2: Run focused tests**

Run:

```powershell
npm run verify:workspace-utils
npm run verify:temporary-workspace-data
npm run verify:temporary-workspace-utils
npm run verify:temporary-session-group
npm run verify:i18n
npm run verify:runtime-html
```

Expected: every command exits 0.

**Step 3: Run the complete verification suite**

Run: `npm run check`

Expected: TypeScript compilation, JavaScript syntax checks, all existing checks, and all new temporary-workspace checks pass.

Run: `git diff --check`

Expected: no whitespace errors.

Run: `git status --short`

Expected: only the intended README change remains before the final commit; `package-lock.json` is unchanged.

**Step 4: Commit documentation**

```bash
git add README.md
git commit -m "docs: explain temporary session grouping"
```

**Step 5: Review final branch scope**

Run:

```powershell
git log --oneline main..HEAD
git diff --stat main...HEAD
```

Expected: design/plan documentation, path classification, payload integration,
frontend grouping, tests, and README changes only. No session files or user
settings are modified.
