Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
45c6304
feat(cli): add /chat file commands for session management
Apr 13, 2026
42ebcd8
fix: address all reviewer feedback from PR #3190
Apr 15, 2026
d3ed275
fix: correct hash calculation from path sanitize to SHA-256
Apr 21, 2026
d6a7932
fix: use runtime context for session ID and handle malformed index JSON
Apr 22, 2026
d22cb29
fix: remove duplicate assertion
lnxsun Apr 25, 2026
bba7b46
fix(chat-save): remove runtime context dependency, UUID from filename
lnxsun Apr 25, 2026
6934e97
fix(chat-delete): use confirm_action built-in command, simplify flow
lnxsun Apr 25, 2026
b05c5db
fix(chat.md): correct hash calculation and session ID source document…
lnxsun Apr 25, 2026
c3d95f5
fix: address review feedback from PR #3190
Apr 26, 2026
50f8a8c
fix: resolve merge conflicts, address review feedback
Apr 26, 2026
90a1089
fix: update source files with -y/--force flag support and session ID …
Apr 26, 2026
411ffe1
Merge branch 'main' into feat/chat-file-commands-clean
lnxsun Apr 26, 2026
3b2677b
fix: address 5 critical review issues from PR #3190
Apr 29, 2026
9fc07a3
fix: address all reviewer feedback from PR #3190
Apr 30, 2026
2fc927f
fix: address remaining critical review issues from PR #3190
Apr 30, 2026
5df87de
fix: address second round of critical review issues from PR #3190
May 1, 2026
ab20329
fix: address third round of review issues from PR #3190
May 2, 2026
76125a8
docs: add notes to sub-commands about direct invocation bypassing rou…
May 6, 2026
898aa20
fix: address all Critical + Suggestion review issues
May 6, 2026
b3f255d
fix: resolve review issues — fix test failures, drop chat-src from de…
Jul 8, 2026
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ package-lock.json
CLAUDE.md

# Qwen Code Configs

.qwen/*
!.qwen/commands/
!.qwen/commands/**
Expand All @@ -40,6 +41,9 @@ CLAUDE.md
.DS_Store
Thumbs.db

# Log files
logs/

# TypeScript build info files
*.tsbuildinfo

Expand All @@ -62,7 +66,8 @@ packages/web-templates/src/generated/
.integration-tests/
packages/vscode-ide-companion/*.vsix

logs/
# Qwen Code Configs

# GHA credentials
gha-creds-*.json

Expand Down
25 changes: 25 additions & 0 deletions .qwen/commands/chat-delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# chat-delete.md — Remove a Session Name from Index

**Note**: Direct invocation (`/chat-delete name`) bypasses the router's argument parsing, locale detection, and name validation. Use `/chat -d name` instead.

1. **Validate `{{name}}`**: `^[a-zA-Z0-9_.-]+$`, ≤128, ≠ `.`/`..`/`__proto__`/`constructor`/`prototype`. Invalid → error, stop.
2. **Read index**: Read `.qwen/chat-index.json` (project root, NOT runtime base). **JSON parse error → output `"chat-index.json is malformed. Fix it manually before deleting."` and stop. Do NOT proceed.**
3. If `{{name}}` NOT found: show list + "Session not in index", stop.
4. **Confirmation**: If user provided `-y` or `--force` flag, SKIP confirmation and delete immediately. Otherwise:
- **STOP and output this exact question:**
```
⚠️ Delete session "{{name}}"?
Type "yes" to confirm, or anything else to cancel:
```
- **WAIT for user's response.** DO NOT proceed until user responds.
- If response = `"yes"` → Continue to delete
- If response ≠ `"yes"` → Output `"Delete cancelled."` and STOP immediately
5. **Delete**: Remove `{{name}}` from index, write back.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Missing atomic write — save has it, delete doesn't

Step 5 says "Remove {{name}} from index, write back" with no atomic-write instruction. Compare with chat-save.md step 6 which explicitly requires: "Write atomically: write to .qwen/.chat-index.json.tmp first, then rename/move to .qwen/chat-index.json."

Both operations modify the same chat-index.json file. An interrupted delete (kill -9, crash, power loss) corrupts the index. All four sub-commands then report "chat-index.json is malformed. Fix it manually." with no self-repair path.

Fix: Mirror chat-save.md step 6: "Write atomically: write to .qwen/.chat-index.json.tmp first, then rename/move to .qwen/chat-index.json. Do NOT write directly to the index file."

— qwen3.7-max via Qwen Code /review

6. **Confirm result**: Output: `Session "{{name}}" removed from index.` + note: "Session file NOT deleted."

**Why file NOT deleted?**

- **Safety**: Deletion is irreversible; removing a name reference is low-risk.
- **Shared reference**: Multiple names can point to the same session. Deleting one name should not destroy data others reference.

**Important**: The index is stored in the **current project's root directory**, NOT the user's home directory or runtime base.
10 changes: 10 additions & 0 deletions .qwen/commands/chat-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# chat-list.md — List All Saved Sessions

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] chat-save.md explicitly stops on malformed .qwen/chat-index.json, but list/resume/delete do not specify parse-error behavior. If the index is truncated or manually corrupted, these commands could treat it as empty/not found or rewrite over it unsafely. Please add the same malformed-JSON stop rule to list/resume/delete and cover it in the validation script.

— gpt-5.5 via Qwen Code /review


**Note**: Direct invocation (`/chat-list`) bypasses the router's argument parsing, locale detection, and name validation. Use `/chat -l` instead.

1. Read `.qwen/chat-index.json` (project root, NOT runtime base). File not found → "No saved sessions." **JSON parse error → output `"chat-index.json is malformed. Fix it manually before listing."` and stop. Do NOT treat as empty.**
2. Display sorted alphabetically: `• <name> (ID: <first8>...)`

**Validation inherited from common rules**: `^[a-zA-Z0-9_.-]+$`, ≤128, ≠ `.`/`..`/`__proto__`/`constructor`/`prototype`.

**Important**: The index is stored in the **current project's root directory**, NOT the user's home directory or runtime base.
36 changes: 36 additions & 0 deletions .qwen/commands/chat-resume.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# chat-resume.md — Resume a Saved Session

**Note**: Direct invocation (`/chat-resume name`) bypasses the router's argument parsing, locale detection, and name validation. Use `/chat -r name` instead.

1. Validate `{{name}}` (Common rules): `^[a-zA-Z0-9_.-]+$`, ≤128, ≠ `.`/`..`/`__proto__`/`constructor`/`prototype`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] This validation does not match the real session ID format accepted by the CLI. qwen --resume accepts standard hyphenated UUIDs, optionally followed by -agent-..., but this rule accepts 32 hex chars or 8-char prefix. A session saved from a normal .jsonl filename can therefore be rejected, while an 8-character prefix can pass this command spec and then fail or resolve incorrectly later. Please align this with the CLI's full session ID format, or resolve prefixes to a unique full session ID before invoking qwen --resume.

— gpt-5.5 via Qwen Code /review

2. Look up ID in index (`.qwen/chat-index.json` in project root, NOT runtime base). **JSON parse error → output `"chat-index.json is malformed. Fix it manually before resuming."` and stop. Do NOT proceed.** Missing/not found → show list + "Session not found", stop.
3. **Validate loaded ID**: The ID from index must match UUID format (`^[a-fA-F0-9-]+$`, allows hyphens). If ID contains any shell metacharacters (`$`, `` ` ``, `;`, `|`, `>`, `<`, `&`, `(`, `)`, spaces), reject it: "Error: Invalid session ID from index. Aborted." — **DO NOT execute any shell command with this ID**.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Session-ID validation regex ^[a-fA-F0-9-]+$ accepts strings starting with - (e.g., -rf, --help, -version). Such an ID becomes an option flag when interpolated into qwen --resume <id> — argv-injection (not shell injection) into the spawned qwen process.

Real session UUIDs don't start with -, but chat-index.json is plain JSON in the project root and chat-save.md explicitly invites manual editing ("Fix it manually before saving."). A malicious or accidentally hand-edited index value like "my-session": "--help" would cause qwen --resume --help instead of resuming. Future qwen flags accepting values (e.g., a hypothetical --config <file>) would let an attacker who can write to .qwen/chat-index.json redirect program behavior on the victim's next /chat -r.

Suggested fix — tighten the validator to a real UUID shape:

The ID from index must match canonical UUID format:
  ^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$
(with optional `-agent-...` suffix matching `(-agent-[a-zA-Z0-9_-]+)?` if needed).

At minimum: reject any ID starting with -.

via Qwen Code /review

4. **Get session project directory**: Read the first line of `<runtimeBase>/projects/<sanitizeCwd>/chats/<id>.jsonl`.
- File missing → "Session file missing", stop.
- File 0 bytes → "Session file empty (likely interrupted save). Aborted.", stop.
- First line not valid JSON → "Session file corrupt at line 1. Aborted.", stop.
- JSON has no `cwd` field → "Session record missing project context. Aborted.", stop.
- Set `<projectRoot>` = the `cwd` field value from the JSON record.
- Verify `<projectRoot>` directory exists on disk. Missing → "Error: original project directory '<projectRoot>' no longer exists. Aborted.", stop.
5. **Verify session belongs to current project**: Apply `sanitizeCwd(<projectRoot>)` and compare with current project's `<sanitizeCwd>`. If they don't match → "Error: Session belongs to another project. Aborted.", stop.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Project-identity verification uses sanitizeCwd() comparison, but the core SessionService uses getProjectHash() (SHA-256) for all project-ownership checks. sanitizeCwd is not collision-resistant — two different paths can produce the same sanitized form (e.g., /home/a-b/c and /home/a/b-c both become -home-a-b-c).

Consider computing SHA-256 via node -e "require('crypto').createHash('sha256').update(process.argv[1]).digest('hex')" '<projectRoot>' to match the core's approach, or document this as a known limitation of file-based commands.

— glm-5.1 via Qwen Code /review


6. **Validate projectRoot for shell safety**: <projectRoot> must match `^[a-zA-Z0-9/._-]+$` — reject any path containing characters outside this set. Reject: "Error: Session path contains unsafe characters. Aborted."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Windows path validation blocks all native Windows resume

The whitelist regex ^[a-zA-Z0-9/._-]+$ rejects both \ and :, which are present in every Windows drive-letter path (e.g., D:\code\project). Since <projectRoot> is sourced from the JSONL cwd field (step 4, line 13), it will always contain these characters on native Windows. Resume will always abort with "Error: Session path contains unsafe characters" for all Windows users.

Fix: Use platform-aware validation. On Windows, allow \ and : (e.g., ^[a-zA-Z0-9/._:\\-]+$), while still rejecting $, backtick, ;, |, >, <, &, (, ), ', ", newlines.

— qwen3.7-max via Qwen Code /review

- For Windows: also reject `^`, `%`, `\`
7. **Execute a shell command** to launch a NEW terminal window with cd to project directory:
- Windows (PowerShell): `start pwsh -NoExit -Command "cd '<projectRoot>'; qwen --resume <id>"`
- Windows (CMD fallback): `start cmd /k "cd /d \"<projectRoot>\" && qwen --resume <id>"` (use if PowerShell unavailable)
- macOS: `osascript -e "tell app \"Terminal\" to do script \"cd '$(echo "<projectRoot>" | sed "s/'/'\\\\''/g")' && qwen --resume <id>\""`
- Linux (WSL): If platform is linux and `/proc/version` contains "Microsoft" or "WSL":
- Convert path: Run `wslpath -w "<projectRoot>"` to get Windows path
- Use: `cmd.exe /c "start cmd /k cd /d \"<windowsPath>\" && qwen --resume <id>"` or prefer `wt.exe -d "<windowsPath>" -- qwen.exe --resume <id>`
- Linux (native): detect terminal with `command -v` (gnome-terminal, xterm, alacritty, kitty in order), then run: `<terminal> -- bash -c "cd '<projectRoot>' && qwen --resume <id>"`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] macOS/Linux terminals close after qwen exits; Windows doesn't

Windows uses -NoExit (PowerShell) and /k (CMD) to keep the terminal window open after qwen exits. But macOS osascript (line 25) and Linux bash -c (line 29) close the terminal when qwen exits.

This means:

  • Windows user: sees the output, can read it at leisure
  • macOS/Linux user: terminal window opens, qwen runs, terminal closes — output may flash and disappear

Fix: For macOS, add && read -p 'Press Enter to close...' after the qwen command in the osascript script. For Linux, use bash -c "... && read -p 'Press Enter to close...'" or switch to --hold flags where available (gnome-terminal supports --hold).

— qwen3.7-max via Qwen Code /review

8. Output: `Session "{{name}}" resumed in new window. (ID: <id>)`

**Runtime Base Resolution** (in priority order):

- `$QWEN_RUNTIME_DIR` (if set)
- `~/.qwen` (default fallback)

**Note**: If user has configured `advanced.runtimeOutputDir` in settings.json, sessions are stored under that path. /chat commands cannot read settings.json (credential leak risk) and will not find those sessions.

**Note**: `<sanitizeCwd>` is the project directory name derived from `sanitizeCwd(projectRoot)`, which replaces all non-alphanumeric characters with `-`. On Windows, the path is also normalized to lowercase before sanitization.
16 changes: 16 additions & 0 deletions .qwen/commands/chat-save.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# chat-save.md — Save Current Session

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The "Runtime Base Resolution + sanitizeCwd + runtimeOutputDir" block at the end of this file is already present verbatim in chat.md's Common Rules. Since chat.md is always loaded before sub-commands, this duplicates ~300 chars. With the total at 13814/15000 chars (only 1186 chars of headroom), removing the duplicate from chat-save.md and chat-resume.md would free ~600 chars for future additions.

Suggested fix: Replace the trailing block with: (See chat.md Common Rules for runtimeBase and sanitizeCwd details.)

— pai/glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Step 5 reads the .jsonl first line for project verification but only handles the case where the cwd field is missing. It does not define behavior when the first line is not valid JSON. chat-resume step 4 explicitly handles this ("Session file corrupt at line 1. Aborted."), but chat-save has no corresponding spec. A corrupt .jsonl would cause unpredictable LLM behavior.

Suggested fix: Add to step 5: "If first line is not valid JSON → skip verification (corrupt session, allow save with warning)" or "First line not valid JSON → Aborted", consistent with chat-resume.

— pai/glm-5.1 via Qwen Code /review

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] FileCommandLoader scans **/*.md recursively, so /chat-save, /chat-list, /chat-resume, /chat-delete all appear as independent slash commands. Direct invocation (e.g., /chat-save my-session) bypasses the router's argument parsing, locale detection, and name validation.

Options: (a) move sub-commands to a non-scanned location (e.g., .qwen/chat-includes/), (b) prefix with _ and update the loader to skip them, or (c) accept the dual-path and add a note to each sub-command that direct invocation skips validation.

— glm-5.1 via Qwen Code /review


**Note**: Direct invocation (`/chat-save name`) bypasses the router's argument parsing, locale detection, and name validation. Use `/chat -s name` instead.

1. Validate `{{name}}`: `^[a-zA-Z0-9_.-]+$`, ≤128, ≠ `.`/`..`/`__proto__`/`constructor`/`prototype`. Invalid → error, stop.
2. Read `.qwen/chat-index.json` (project root, NOT runtime base). File not found → `{}`. **JSON parse error → output `"chat-index.json is malformed. Fix it manually before saving."` and stop. Do NOT overwrite.**
3. If `{{name}}` in index → ask "Overwrite? (yes/no)". ≠ yes → stop.
4. Session ID = **newest `.jsonl` file by modification time** in `<runtimeBase>/projects/<sanitizeCwd>/chats/`. The filename (without `.jsonl`) IS the session UUID. ⚠️ **IMPORTANT**: If wrong session is saved, resume the target session first, then run `/chat -s`. No .jsonl found → "No session found. Start a conversation first.", stop.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] chat-save selects the newest .jsonl by mtime without verifying the session belongs to the current project. The same diff added project-identity verification to chat-resume (steps 4–5: read JSONL cwd, compare via sanitizeCwd), but chat-save has no equivalent check. When sanitizeCwd collides across project paths, chat-save can store a session ID from a different project — and chat-resume will then reject it with "Session belongs to another project."

Consider adding: after selecting the newest .jsonl, read its first line and verify cwd matches the current project before saving.

— glm-5.1 via Qwen Code /review

5. **Verify session belongs to current project**: Read the first line of the selected `.jsonl` file.
- If JSON has no `cwd` field → skip verification (legacy session, allow save).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Save/resume asymmetry: save allows sessions that resume will always reject

chat-save step 5 allows saving sessions with no cwd field ("skip verification, legacy session"). But chat-resume step 4 (line 12) aborts on the same condition: "Session record missing project context. Aborted."

Users get a successful save confirmation (Saved: name → <id>) for a session that can never be resumed. This is a silent data integrity issue — the user believes the session is saved and recoverable, but it's actually a dead entry.

Fix: Make verification symmetric — either both allow with warning, or both abort. Recommended: chat-save should warn "Warning: session has no cwd field — this session will NOT be resumable. Save anyway? (yes/no)" before proceeding.

— qwen3.7-max via Qwen Code /review

- First line not valid JSON → skip verification (corrupt session, allow save with warning "Warning: session file corrupt, skipping project verification.").

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Saves corrupt sessions without warning they're unresumable

"First line not valid JSON → skip verification (corrupt session, allow save with warning)." The warning is ephemeral — shown once in the chat output. But the index entry persists permanently.

When the user runs /chat -r <name> weeks later, they get: "Session file corrupt at line 1. Aborted." — with no connection to the save-time warning they saw once.

Fix: Either refuse to save corrupt sessions (matching resume's strictness), or change the warning to explicitly state the consequence: "Warning: session file corrupt — this session will NOT be resumable via /chat -r. Save anyway? (yes/no)"

— qwen3.7-max via Qwen Code /review

- Apply `sanitizeCwd(<cwd>)` and compare with current project's `<sanitizeCwd>`. If they don't match → "Error: Selected session belongs to another project. Aborted. Please resume the session from its original project first.", stop.
6. Add or update `{{name}}` key in existing index object (2-space indent). **Write atomically**: write to `.qwen/.chat-index.json.tmp` first, then rename/move to `.qwen/chat-index.json`. Do NOT write directly to the index file.
7. Output: `Saved: {{name}} → <id>` (or `Overwritten: ...`)

Runtime Base / sanitizeCwd: see chat.md Common Rules.
127 changes: 127 additions & 0 deletions .qwen/commands/chat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
description: Chat session manager. /chat [-s|-l|-r|-d|-h] [name] [-y|--force]
---

# CRITICAL: First check {{args}}, then route

## Step 0: Immediate Validation (MUST execute FIRST)

**Check `{{args}}` right now, before doing anything else:**

1. Is `{{args}}` empty? → **Show Help immediately, STOP**
2. Is `{{args}}` only whitespace? → **Show Help immediately, STOP**
3. Does the first token look like a valid flag? (`-s`, `--save`, `-l`, `--list`, `-r`, `--resume`, `-d`, `--delete`, `-h`, `--help`)
- **NO** → invalid flag/unrecognized → **Show Help immediately, STOP**
- **YES** → Continue to Step 1

**⚠️ DO NOT skip this step. DO NOT proceed with any action until you verify `{{args}}`.**

---

## Step 1: Detect Environment

### Language

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Reading the full user-level ~/.qwen/settings.json just to determine general.language can expose unrelated sensitive configuration to the model context. That file may contain provider credentials, MCP server headers/tokens, environment values, or other auth-related settings. Please avoid loading the full settings file from this Markdown command; use the current conversation language or a safe mechanism that exposes only the language setting.

— gpt-5.5 via Qwen Code /review

Run `node -e "console.log(Intl.DateTimeFormat().resolvedOptions().locale)"` to get system locale.
Use the language code (first 2 chars, e.g., "en", "zh", "ja") to determine response language.
If locale detection fails, match the language the user used in their prompt.

### OS Detection (ONLY for `-r`/`--resume`)

**Skip this step for other flags.** Only run when `-r` is detected.

Run `node -e "console.log(process.platform)"`. Works across all shells.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] OS detection runs node -e "console.log(process.platform)" then routes by string. WSL2 returns linux, so the router routes to chat-resume.md's Linux branch — gnome-terminal -- bash -c "…" or xterm -e "…". Inside WSL these either fail (no X display) or pop up an X window the user can't easily reach, and qwen --resume <id> runs in that lost window.

WSL is a non-trivial slice of "Windows" qwen-code users. Repro path: "I /chat -r foo, nothing happens." No log to inspect (see also chat.md re: missing breadcrumbs). Hours of debug.

Suggested fix — when process.platform === 'linux', additionally test for WSL:

If platform == 'linux':
  Read /proc/version. If it contains 'Microsoft' or 'WSL' (case-insensitive),
  treat as Windows for the resume branch — use Windows Terminal:
    wt.exe -d '<projectRoot>' -- qwen.exe --resume <id>
  (or fall back to: cmd.exe /c start cmd /k "cd /d <projectRoot> && qwen --resume <id>")

Document this branch in chat-resume.md Step 6 alongside the existing platform list.

via Qwen Code /review


- `win32` → Windows
- `linux` → Linux (including WSL — detect WSL separately, see chat-resume.md)
- `darwin` → macOS

**WSL Detection**: If platform is `linux`, additionally read `/proc/version`. If it contains "Microsoft" or "WSL" (case-insensitive), treat as Windows for resume — use Windows Terminal or CMD.

---

## Step 2: Parse and Route

Split `{{args}}` by whitespace. First token = flag. Remaining = raw_args.

| Flag | Action | Sub-Command File |
| ----------------- | ----------------------------------------- | ---------------- |
| `-s` / `--save` | Go to Step 3 | `chat-save.md` |
| `-l` / `--list` | Read `chat-list.md` and execute its logic | `chat-list.md` |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The routing table instructs the LLM to "Read chat-list.md and execute its logic" / "Read corresponding sub-command file and execute". Per packages/cli/src/services/command-factory.ts:125-150, file commands return submit_prompt with the rendered markdown — there is no built-in include/transclude mechanism in the command processor. The pattern works only because the LLM voluntarily uses its file-reading tool when prompted. None of the 240 assertions in test.mjs exercise the actual routing — they all do String.prototype.includes() over the raw markdown.

Future-maintainer hazard: someone adding /chat -e <name> (export) will copy the row pattern, ship a feature that fails silently when an LLM (especially smaller / cheaper variants) decides to skip the file read. There's no test that catches it.

Suggested fix — pick one:

  1. Document the convention explicitly in CHAT-DESIGN.md: "Routing relies on LLM tool-use, not runtime expansion. Verified via prompt behavior, not the test suite."
  2. Add at least one integration-style test that boots the command processor, runs /chat -l, and asserts the LLM-facing prompt actually instructs reading chat-list.md.
  3. Inline the sub-commands into chat.md if size budget allows — kills the routing-via-file-read pattern entirely.

via Qwen Code /review

| `-r` / `--resume` | Go to Step 3 | `chat-resume.md` |
| `-d` / `--delete` | Go to Step 3 | `chat-delete.md` |
| `-h` / `--help` | **Show Help immediately, STOP** | — |

### Step 3: Validate name (for `-s`, `-r`, `-d`)

**For delete (`-d`):**

1. Parse raw_args to extract name: Filter out `-y` and `--force` flags first, the first remaining token is the name.
2. If name is missing, empty, or whitespace only → **Show Help immediately, STOP**
3. If extra non-flag tokens remain after the first name → **Show Help immediately, STOP**
4. If `-y` or `--force` was found → Set `forceDelete = true`

**For save/resume (`-s`, `-r`):**

1. Parse raw_args to extract name: the first remaining token is the name.
- **Reject any token starting with `-`** (e.g., `-y`, `--force` are delete-only options)
- If extra non-flag tokens remain after the first name → Output: `Error: Unexpected token: <token>. /chat -s|-r takes only a single name.` and STOP
2. If name is missing, empty, or whitespace only → **Show Help immediately, STOP**

**Common validation:**

- Does name match `^[a-zA-Z0-9_.-]+$` and length ≤ 128?
- **NO** → Output error: `Invalid name. Must match: ^[a-zA-Z0-9_.-]+$ (max 128 chars)` and STOP
- **YES** → Check if name is reserved (`.`, `..`, `__proto__`, `constructor`, `prototype`)
- **YES, reserved** → Output error: `Invalid name. Reserved: ., .., __proto__, constructor, prototype` and STOP
- **NO, not reserved** → Read corresponding sub-command file and execute

---

## Common Rules

| Rule | Value |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Valid name regex** | `^[a-zA-Z0-9_.-]+$` |
| **Max length** | 128 characters |
| **Reserved names** | `.`, `..`, `__proto__`, `constructor`, `prototype` |
| **Index path** | `.qwen/chat-index.json` (project root) |
| **Index format** | `{"name": "sessionId", ...}` |
| **Session ID source** | Filename (no extension) of `.jsonl` in `<runtimeBase>/projects/<sanitizeCwd>/chats/`. runtimeBase priority: `$QWEN_RUNTIME_DIR` > `~/.qwen` (default) |
| **Project dir** | `sanitizeCwd(projectRoot)` replaces all non-alphanumeric characters with `-`. On Windows, also lowercase. E.g., `D:\code\qwen-code` → `d--code-qwen-code` |

---

## Help Text

**Show this when:**

- `{{args}}` is empty or whitespace only
- First token is NOT a valid flag
- Flag requires name but name is missing/empty
- User explicitly requests `-h` or `--help`

**Display this exact text and STOP all processing:**

```
Chat Session Manager

Usage: /chat <flag> [name] [-y|--force]

Flags:
-s, --save <name> Save current session with a name
-l, --list List all saved sessions
-r, --resume <name> Resume a saved session
-d, --delete <name> Delete a saved session from index
-h, --help Show this help

Options:
-y, --force Skip confirmation prompt (for -d)

Examples:
/chat -s my-session
/chat -l
/chat -r my-session
/chat -d my-session
/chat -d my-session -y # Delete without confirmation
```