Skip to content

feat: add /chat command for saving, listing, resuming, and deleting named sessions#3105

Open
lnxsun wants to merge 4 commits intoQwenLM:mainfrom
lnxsun:feat/chat-session-command
Open

feat: add /chat command for saving, listing, resuming, and deleting named sessions#3105
lnxsun wants to merge 4 commits intoQwenLM:mainfrom
lnxsun:feat/chat-session-command

Conversation

@lnxsun
Copy link
Copy Markdown

@lnxsun lnxsun commented Apr 10, 2026

目标:
我想给qwen code开发一个/chat命令,用其来保存、查看、恢复和删除会话。
实现的 /chat 子命令:**

  • /chat save [name] - 给当前会话命名保存
  • /chat list - 列出已命名保存的会话
  • /chat resume [name] - 按名字恢复会话
  • /chat delete [name] - 删除已命名的会话
    Related to iflow cli的好功能接过来 #3025

@lnxsun
Copy link
Copy Markdown
Author

lnxsun commented Apr 10, 2026

我已经在本地重新构建了cli.js并逐一测试了/chat的4个子命令(save、list、resume、delete)的功能,对于一开始存在的resume不能跳转到原save的会话的问题也进行了修正和复测通过,确认都能实现预期功能,请测试并拉取

* 读取索引文件
* @returns 索引对象,如果文件不存在则返回空对象
*/
export async function readChatIndex(): Promise<ChatIndex> {
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] readChatIndex() currently turns every read/parse failure into {}. That means permission errors and other I/O failures are silently reported to callers as "no saved sessions" / "session not found", which makes the new /chat commands lose data visibility without any actionable error.

Suggested change
export async function readChatIndex(): Promise<ChatIndex> {
export async function readChatIndex(): Promise<ChatIndex> {
try {
const content = await fs.readFile(getIndexPath(), 'utf-8');
return JSON.parse(content) as ChatIndex;
} catch (error) {
if (error instanceof SyntaxError) {
return {};
}
if (
typeof error === 'object' &&
error !== null &&
'code' in error &&
error.code === 'ENOENT'
) {
return {};
}
throw error;
}
}

— gpt-5.4 via Qwen Code /review

/**
* 获取索引文件路径
*/
function getIndexPath(): string {
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 index is written to a single global ~/.qwen/chat-index.json, but session lookup/removal is project-scoped through SessionService(cwd). In practice that means /chat list can show names saved in other repos that /chat resume cannot load here, and the index also ignores custom runtime roots such as QWEN_RUNTIME_DIR.

Suggested change
function getIndexPath(): string {
function getIndexPath(): string {
return path.join(new Storage(process.cwd()).getProjectDir(), 'chat-index.json');
}

The exact storage helper may differ, but the index needs to follow the same project/runtime scoping rules as session storage.

— gpt-5.4 via Qwen Code /review

if (!deleted) {
return {
type: 'message',
messageType: 'error',
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 delete <name> only removes the alias from chat-index.json; it never deletes the underlying session file. That makes the command behave like "unlink alias" rather than "delete session", which is surprising for users and leaves conversation data on disk.

Suggested change
messageType: 'error',
const sessionId = await getSessionIdByName(name);
if (!sessionId) {
return {
type: 'message',
messageType: 'error',
content: t('Session "{{name}}" not found.', { name }),
};
}
const config = context.services.config;
if (!config) {
return {
type: 'message',
messageType: 'error',
content: t('Config not loaded.'),
};
}
const sessionService = new SessionService(config.getTargetDir());
const removed = await sessionService.removeSession(sessionId);
if (!removed) {
return {
type: 'message',
messageType: 'error',
content: t('Session "{{name}}" not found.', { name }),
};
}
await deleteSessionFromIndex(name);

— gpt-5.4 via Qwen Code /review

'test-session-1',
);

expect(result).toEqual({
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 test is still asserting the old resume behavior, but the implementation now returns a dialog: 'resume' action and requires a valid CommandContext. The focused test run is currently red because this case calls resumeCommand.action(undefined as any, ...) and then expects an info message containing Found session.

Suggested change
expect(result).toEqual({
const mockContext: CommandContext = {
services: {
config: {
getTargetDir: () => '/tmp/project',
},
},
ui: {} as never,
executionMode: 'non_interactive',
};
const result = await resumeCommand?.action!(mockContext, 'test-session-1');
expect(result).toEqual({
type: 'dialog',
dialog: 'resume',
params: { sessionId: 'test-session-id-12345' },
});

— gpt-5.4 via Qwen Code /review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants