Skip to content

Feature /resume command to continue session#390

Open
yashksaini-coder wants to merge 16 commits intoNano-Collective:mainfrom
yashksaini-coder:feature/51-resume-command
Open

Feature /resume command to continue session#390
yashksaini-coder wants to merge 16 commits intoNano-Collective:mainfrom
yashksaini-coder:feature/51-resume-command

Conversation

@yashksaini-coder
Copy link
Contributor

Fixes Issue: #51

Description

Adds automatic session storage and a /resume command so users can save, list, and restore chat sessions. Sessions are stored under a configurable directory (default ~/.nanocoder-sessions), with an index and one JSON file per session. Autosave keeps a single “current” session updated (debounced); clearing the chat or resuming another session starts a new one. /resume with no args opens an interactive session selector; /resume last, /resume , and /resume resume directly. Aliases /sessions and /history behave like /resume. Resuming restores messages, provider, and model.

Also: security – pnpm overrides for minimatch, markdown-it, ajv, qs, and hono to address audit findings; Copilot – ESM fix in test-copilot.sh, hermetic copilot-credentials spec with NANOCODER_CONFIG_DIR, apiKey: 'dummy-key' for Copilot provider, OAuth naming (pollForOAuthToken / githubOAuthToken), and credential file mode 0o600; lockfile – fix duplicate keys in pnpm-lock.yaml; audit – add pnpm run audit script.

Type of Change

[ ] Bug fix
[x] New feature
[ ] Breaking change
[ ] Documentation update

Testing

[x] New features include passing tests in .spec.ts/tsx files
[x] All existing tests pass (pnpm test:all completes successfully)
[x] Tests cover both success and error scenarios

Checklist

[x] Code follows project style guidelines
[x] Self-review completed
[ ] Documentation updated (if needed)
[x] No breaking changes (or clearly documented)
[x] Appropriate logging added using structured logging (see CONTRIBUTING.md)

yashksaini-coder and others added 9 commits March 1, 2026 20:31
- Introduced session configuration options including autosave, save interval, max sessions, retention days, and directory.
- Implemented session loading from project-level, global, and home directory configurations.
- Added a new command to resume previous chat sessions in the help documentation.
- Integrated session autosave hook in the main application component.
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 5.81.0 to 5.85.0.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@5.85.0/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 5.85.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [cheerio](https://github.com/cheeriojs/cheerio) from 1.1.2 to 1.2.0.
- [Release notes](https://github.com/cheeriojs/cheerio/releases)
- [Commits](cheeriojs/cheerio@v1.1.2...v1.2.0)

---
updated-dependencies:
- dependency-name: cheerio
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@ai-sdk/openai-compatible](https://github.com/vercel/ai) from 2.0.27 to 2.0.30.
- [Release notes](https://github.com/vercel/ai/releases)
- [Changelog](https://github.com/vercel/ai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vercel/ai/compare/@ai-sdk/openai-compatible@2.0.27...@ai-sdk/openai-compatible@2.0.30)

---
updated-dependencies:
- dependency-name: "@ai-sdk/openai-compatible"
  dependency-version: 2.0.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@ai-sdk/google](https://github.com/vercel/ai) from 3.0.30 to 3.0.33.
- [Release notes](https://github.com/vercel/ai/releases)
- [Changelog](https://github.com/vercel/ai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vercel/ai/compare/@ai-sdk/google@3.0.30...@ai-sdk/google@3.0.33)

---
updated-dependencies:
- dependency-name: "@ai-sdk/google"
  dependency-version: 3.0.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [ai](https://github.com/vercel/ai) from 6.0.97 to 6.0.104.
- [Release notes](https://github.com/vercel/ai/releases)
- [Changelog](https://github.com/vercel/ai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vercel/ai/compare/ai@6.0.97...ai@6.0.104)

---
updated-dependencies:
- dependency-name: ai
  dependency-version: 6.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Addresses feedback from PR Nano-Collective#383 review:

- Extract getSafeMemory() and getSafeCpuUsage() into shared module
  (source/utils/logging/safe-process.ts)
- Import safe functions in performance.ts, request-tracker.ts,
  memory-check.ts, and health-monitor.ts
- Fix double call to getSafeMemory() in takePerformanceSnapshot()
  by capturing value once and reusing
- Wrap direct process.memoryUsage() calls in chat-handler.ts and
  mcp-client.ts with getSafeMemory()
- All files properly import from node:process via shared module

Files modified:
- source/utils/logging/safe-process.ts (new)
- source/utils/logging/performance.ts
- source/utils/logging/request-tracker.ts
- source/utils/logging/health-monitor/checks/memory-check.ts
- source/utils/logging/health-monitor/core/health-monitor.ts
- source/ai-sdk-client/chat/chat-handler.ts
- source/mcp/mcp-client.ts
Copilot AI review requested due to automatic review settings March 1, 2026 16:47
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class session persistence and a /resume command so users can autosave and restore chat sessions across runs, plus dependency override updates to address security audit findings.

Changes:

  • Introduces on-disk session storage (sessions.json index + per-session JSON) with autosave and retention/limits.
  • Adds /resume (and aliases) with an interactive selector UI and direct resume modes (last, <id>, <n>), wiring it into app state/handlers.
  • Updates configuration loading/types to support nanocoder.sessions.*, and applies pnpm overrides + adds an audit script.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
source/types/config.ts Adds sessions config shape to AppConfig.
source/types/app.ts Extends MessageSubmissionOptions with session selector/resume callbacks.
source/config/index.ts Loads/validates session configuration from config files with defaults.
source/session/session-manager.ts Implements session CRUD, index maintenance, retention, and limits.
source/hooks/useSessionAutosave.ts Adds debounced autosave hook for the current conversation.
source/hooks/useAppState.tsx Adds isSessionSelectorMode + currentSessionId to app state.
source/hooks/useAppHandlers.tsx Adds handlers to enter selector mode and apply selected sessions.
source/components/session-selector.tsx New Ink-based selector UI for resuming sessions.
source/app/components/modal-selectors.tsx Adds session selector as a modal mode.
source/app/utils/app-util.ts Adds /resume//sessions//history slash-command handling.
source/commands/resume.ts Adds a registry command entry (help/info handler).
source/commands/index.ts Exports the new resume command.
source/commands/help.tsx Mentions /resume in the help UI.
source/app/App.tsx Wires in autosave + selector mode rendering (but currently breaks imports).
package.json Adds audit script and security overrides.
pnpm-lock.yaml Applies override/resolution updates for audited packages.
docs/session-management.md Documents session storage, config, and /resume usage.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +70 to +72
const sessionId = Date.now().toString();
const timestamp = new Date().toISOString();

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

Session IDs are generated with Date.now().toString(), which can collide if multiple sessions are created within the same millisecond (e.g., fast clears/resumes or concurrent autosave logic), potentially overwriting an existing session file. Use a collision-resistant ID (e.g., crypto.randomUUID() or timestamp + random suffix).

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +123
await fs.writeFile(sessionFilePath, JSON.stringify(session, null, 2), {
mode: 0o600,
});

// Update sessions index
const sessions = await this.listSessions();
const existingSessionIndex = sessions.findIndex(s => s.id === session.id);

const sessionMetadata: SessionMetadata = {
id: session.id,
title: session.title,
createdAt: session.createdAt,
lastAccessedAt: new Date().toISOString(),
messageCount: session.messageCount,
provider: session.provider,
model: session.model,
workingDirectory: session.workingDirectory,
};

if (existingSessionIndex >= 0) {
sessions[existingSessionIndex] = sessionMetadata;
} else {
sessions.push(sessionMetadata);
}

await fs.writeFile(
this.sessionsIndexPath,
JSON.stringify(sessions, null, 2),
{mode: 0o600},
);
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

fs.writeFile(..., {mode: 0o600}) only applies mode when the file is created; subsequent writes will preserve any existing (possibly too-permissive) mode. For session files and sessions.json, consider enforcing permissions explicitly (e.g., chmod after write) so existing files are corrected as well.

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +122
// Update sessions index
const sessions = await this.listSessions();
const existingSessionIndex = sessions.findIndex(s => s.id === session.id);

const sessionMetadata: SessionMetadata = {
id: session.id,
title: session.title,
createdAt: session.createdAt,
lastAccessedAt: new Date().toISOString(),
messageCount: session.messageCount,
provider: session.provider,
model: session.model,
workingDirectory: session.workingDirectory,
};

if (existingSessionIndex >= 0) {
sessions[existingSessionIndex] = sessionMetadata;
} else {
sessions.push(sessionMetadata);
}

await fs.writeFile(
this.sessionsIndexPath,
JSON.stringify(sessions, null, 2),
{mode: 0o600},
);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

saveSession() does a read-modify-write of sessions.json without any locking/serialization. Autosave timers and /resume can call into this concurrently, which can cause lost updates (last writer wins) or inconsistent metadata ordering. Consider serializing writes with a simple mutex/queue inside SessionManager (or writing index updates via a single atomic operation).

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +19
const [loading, setLoading] = useState(true);
const {} = useApp();

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

const {} = useApp(); is unused and will trigger noUnusedLocals/lint errors. Remove the useApp import and this line (or use the returned app instance if intended).

Copilot uses AI. Check for mistakes.
Comment on lines +875 to +982
/**
* Handles /resume, /sessions, /history (session resume).
* No args: show session selector. One arg: resume by "last", id, or list index.
* Returns true if handled.
*/
async function handleResumeCommand(
commandParts: string[],
options: MessageSubmissionOptions,
): Promise<boolean> {
const commandName = commandParts[0]?.toLowerCase();
if (!commandName || !isResumeCommand(commandName)) {
return false;
}

const {
onAddToChatQueue,
onEnterSessionSelectorMode,
onResumeSession,
onCommandComplete,
getNextComponentKey,
} = options;

if (!onEnterSessionSelectorMode || !onResumeSession) {
onCommandComplete?.();
return true;
}

const args = commandParts.slice(1);

// No args: show session selector
if (args.length === 0) {
try {
await sessionManager.initialize();
onEnterSessionSelectorMode();
} catch (error) {
onAddToChatQueue(
React.createElement(ErrorMessage, {
key: `resume-error-${getNextComponentKey()}`,
message: `Failed to initialize sessions: ${getErrorMessage(error)}`,
hideBox: true,
}),
);
onCommandComplete?.();
}
return true;
}

// One arg: resolve and load session
const sessionIdOrSpecial = args[0];
try {
await sessionManager.initialize();
const sessions = await sessionManager.listSessions();
const sorted = [...sessions].sort(
(a, b) =>
new Date(b.lastAccessedAt).getTime() -
new Date(a.lastAccessedAt).getTime(),
);

let sessionId: string | null = null;

if (sessionIdOrSpecial.toLowerCase() === 'last') {
if (sorted.length > 0) sessionId = sorted[0].id;
} else {
const index = Number.parseInt(sessionIdOrSpecial, 10);
if (!Number.isNaN(index) && index >= 1 && index <= sorted.length) {
sessionId = sorted[index - 1].id;
} else {
sessionId = sessionIdOrSpecial;
}
}

if (!sessionId) {
onAddToChatQueue(
React.createElement(InfoMessage, {
key: `resume-info-${getNextComponentKey()}`,
message: 'No sessions found.',
hideBox: true,
}),
);
onCommandComplete?.();
return true;
}

const session = await sessionManager.loadSession(sessionId);
if (session) {
onResumeSession(session);
} else {
onAddToChatQueue(
React.createElement(ErrorMessage, {
key: `resume-error-${getNextComponentKey()}`,
message: `Session not found: ${sessionId}`,
hideBox: true,
}),
);
}
} catch (error) {
onAddToChatQueue(
React.createElement(ErrorMessage, {
key: `resume-error-${getNextComponentKey()}`,
message: `Failed to resume session: ${getErrorMessage(error)}`,
hideBox: true,
}),
);
}

onCommandComplete?.();
return true;
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

handleResumeCommand introduces new command behavior (/resume + aliases, including selector mode and direct resume). There are existing AVA tests for command parsing/handling in this module (app-util.spec.ts), but no tests were added for the new resume behavior. Add tests covering at least: /resume (enter selector mode), /resume last, /resume <n>, invalid session id (error message), and the alias commands (/sessions, /history).

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +57
// Initialize session manager
useEffect(() => {
const initialize = async () => {
if (!initializedRef.current) {
try {
await sessionManager.initialize();
initializedRef.current = true;
} catch (error) {
console.warn('Failed to initialize session manager:', error);
}
}
};

void initialize();

return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

useSessionAutosave initializes sessionManager unconditionally on mount, even when sessions.autoSave is disabled. That still creates the sessions directory / index and runs retention cleanup, which may be surprising when autosave is turned off. Consider checking config first and only initializing when autosave is enabled (or when the user explicitly runs /resume).

Copilot uses AI. Check for mistakes.
@will-lamerton
Copy link
Member

Hey @yashksaini-coder - thanks for this PR - a long overdue feature! :D

I see that most PR checks are failing due to pnpm install failing. Are you able to correct this so the checks can run? I'll review once these have run properly :)

yashksaini-coder and others added 7 commits March 3, 2026 11:21
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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.

4 participants