-
Notifications
You must be signed in to change notification settings - Fork 4
feat: default sessions list to cwd project + fix sqlite timestamps #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
buihongduc132
wants to merge
9
commits into
shuv1337:master
Choose a base branch
from
buihongduc132:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
62c69ed
test: expand CLI coverage for sqlite rename/copy and root/TUI behavior
3cecb34
feat: default sessions list to cwd project, fix sqlite timestamps
c7a55a1
fix: add --global flag to tests after cwd filtering implementation
bc649aa
test: add edge case tests exposing symlink resolution bug
98dea05
fix: resolve symlinks in worktree paths for cwd filtering
b87a062
fix: stabilize CLI error output and clipboard handling
00bb995
feat: auto-detect SQLite vs JSONL based on latest session timestamp
43a7d51
chore: update .gitignore to exclude .opencode directory
0a98e80
feat: add oqo-sess binary alias for opencode-manager
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |||||
| */ | ||||||
|
|
||||||
| import { Command, type OptionValues } from "commander" | ||||||
| import { realpath } from "node:fs/promises" | ||||||
| import { parseGlobalOptions, type GlobalOptions } from "../index" | ||||||
| import { | ||||||
| copySession, | ||||||
|
|
@@ -44,6 +45,8 @@ function collectOptions(cmd: Command): OptionValues { | |||||
| export interface SessionsListOptions { | ||||||
| /** Filter sessions by project ID */ | ||||||
| project?: string | ||||||
| /** List all sessions globally (conflicts with --project) */ | ||||||
| global?: boolean | ||||||
| /** Search query to filter sessions (fuzzy match) */ | ||||||
| search?: string | ||||||
| } | ||||||
|
|
@@ -104,12 +107,14 @@ export function registerSessionsCommands(parent: Command): void { | |||||
| .command("list") | ||||||
| .description("List sessions") | ||||||
| .option("-p, --project <projectId>", "Filter by project ID") | ||||||
| .option("-g, --global", "List all sessions globally (default: sessions for current directory)") | ||||||
| .option("-s, --search <query>", "Search query to filter sessions") | ||||||
| .action(function (this: Command) { | ||||||
| const globalOpts = parseGlobalOptions(collectOptions(this)) | ||||||
| const cmdOpts = this.opts() | ||||||
| const listOpts: SessionsListOptions = { | ||||||
| project: cmdOpts.project as string | undefined, | ||||||
| global: cmdOpts.global as boolean | undefined, | ||||||
| search: cmdOpts.search as string | undefined, | ||||||
| } | ||||||
| handleSessionsList(globalOpts, listOpts) | ||||||
|
|
@@ -197,6 +202,9 @@ export function registerSessionsCommands(parent: Command): void { | |||||
| [ | ||||||
| "", | ||||||
| "Examples:", | ||||||
| " opencode-manager sessions list # List sessions for current directory", | ||||||
| " opencode-manager sessions list --global # List all sessions globally", | ||||||
| " opencode-manager sessions list -p my-project # List sessions for specific project", | ||||||
| " opencode-manager sessions list --experimental-sqlite", | ||||||
| " opencode-manager sessions list --db ~/.local/share/opencode/opencode.db", | ||||||
| ].join("\n") | ||||||
|
|
@@ -216,20 +224,113 @@ function buildSessionSearchText(session: SessionRecord): string { | |||||
| ].join(" ").replace(/\s+/g, " ").trim() | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Result of inferring project from cwd. | ||||||
| */ | ||||||
| interface InferredProject { | ||||||
| projectId: string | ||||||
| worktree: string | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Infer the project ID from the current working directory. | ||||||
| * Finds projects whose worktree is a parent of (or equal to) cwd, | ||||||
| * then selects the deepest match (longest path). | ||||||
| * | ||||||
| * @throws UsageError if no project matches cwd or if multiple projects match at the same depth | ||||||
| */ | ||||||
| async function inferProjectFromCwd( | ||||||
| provider: ReturnType<typeof createProviderFromGlobalOptions> | ||||||
| ): Promise<InferredProject> { | ||||||
| const projects = await provider.loadProjectRecords() | ||||||
| const cwd = process.cwd() | ||||||
|
|
||||||
| // Find all projects whose worktree is a parent of or equal to cwd | ||||||
| const candidates: Array<{ project: typeof projects[0]; depth: number }> = [] | ||||||
|
|
||||||
| for (const project of projects) { | ||||||
| let worktree = project.worktree | ||||||
| try { | ||||||
| // Resolve symlinks to get the real path (cwd is already resolved by process.cwd()) | ||||||
| worktree = await realpath(worktree) | ||||||
| } catch { | ||||||
| // If realpath fails (path doesn't exist), use original worktree path | ||||||
| worktree = project.worktree | ||||||
| } | ||||||
| // Normalize to remove trailing slashes for consistent comparison | ||||||
| worktree = worktree.replace(/\/+$/, "") | ||||||
| // Check if cwd is inside or equal to worktree | ||||||
| if (cwd === worktree || cwd.startsWith(worktree + "/")) { | ||||||
| // Depth is the number of path segments in worktree | ||||||
| const depth = worktree.split("/").length | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hardcoded
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/cli/commands/sessions.ts
Line: 255
Comment:
hardcoded `/` separator won't work on Windows - use `path.sep` instead
```suggestion
const depth = worktree.split(path.sep).length
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||
| candidates.push({ project, depth }) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if (candidates.length === 0) { | ||||||
| throw new UsageError( | ||||||
| `No project found for current directory: ${cwd}\n` + | ||||||
| `Use --global to list all sessions, or --project <id> to specify a project.` | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| // Find the maximum depth | ||||||
| const maxDepth = Math.max(...candidates.map((c) => c.depth)) | ||||||
|
|
||||||
| // Filter to only candidates at max depth | ||||||
| const deepestCandidates = candidates.filter((c) => c.depth === maxDepth) | ||||||
|
|
||||||
| if (deepestCandidates.length > 1) { | ||||||
| const projectIds = deepestCandidates.map((c) => c.project.projectId).join(", ") | ||||||
| throw new UsageError( | ||||||
| `Ambiguous project match for current directory: ${cwd}\n` + | ||||||
| `Multiple projects match at the same depth: ${projectIds}\n` + | ||||||
| `Use --project <id> to specify which project.` | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| const winner = deepestCandidates[0].project | ||||||
| return { | ||||||
| projectId: winner.projectId, | ||||||
| worktree: winner.worktree, | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Handle the sessions list command. | ||||||
| */ | ||||||
| async function handleSessionsList( | ||||||
| globalOpts: GlobalOptions, | ||||||
| listOpts: SessionsListOptions | ||||||
| ): Promise<void> { | ||||||
| // Validate that --global and --project are not used together | ||||||
| if (listOpts.global && listOpts.project) { | ||||||
| throw new UsageError( | ||||||
| "Cannot use --global and --project together. Use one or the other." | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| // Create data provider based on global options (JSONL or SQLite backend) | ||||||
| const provider = createProviderFromGlobalOptions(globalOpts) | ||||||
|
|
||||||
| // Determine the project filter | ||||||
| let projectIdFilter: string | undefined | ||||||
| if (listOpts.global) { | ||||||
| // --global: list all sessions, no project filter | ||||||
| projectIdFilter = undefined | ||||||
| } else if (listOpts.project) { | ||||||
| // --project specified: use it | ||||||
| projectIdFilter = listOpts.project | ||||||
| } else { | ||||||
| // Default: infer project from current working directory | ||||||
| const inferred = await inferProjectFromCwd(provider) | ||||||
| projectIdFilter = inferred.projectId | ||||||
| } | ||||||
|
|
||||||
| // Load session records from the data layer | ||||||
| // If a project filter is provided, pass it to the loader | ||||||
| let sessions = await provider.loadSessionRecords({ | ||||||
| projectId: listOpts.project, | ||||||
| projectId: projectIdFilter, | ||||||
| }) | ||||||
|
|
||||||
| // Apply fuzzy search if search query is provided | ||||||
|
|
@@ -489,21 +590,26 @@ async function handleSessionsCopy( | |||||
| ): Promise<void> { | ||||||
| const outputOpts = getOutputOptions(globalOpts) | ||||||
|
|
||||||
| // Create data provider based on global options (JSONL or SQLite backend) | ||||||
| const provider = createProviderFromGlobalOptions(globalOpts) | ||||||
|
|
||||||
| // Resolve session ID to a session record | ||||||
| const { session } = await resolveSessionId(copyOpts.session, { | ||||||
| root: globalOpts.root, | ||||||
| allowPrefix: true, | ||||||
| provider, | ||||||
| }) | ||||||
|
|
||||||
| // Validate target project exists | ||||||
| // Use prefix matching for convenience, but require exactly one match | ||||||
| const { project: targetProject } = await resolveProjectId(copyOpts.to, { | ||||||
| root: globalOpts.root, | ||||||
| allowPrefix: true, | ||||||
| provider, | ||||||
| }) | ||||||
|
|
||||||
| // Copy the session | ||||||
| const newRecord = await copySession(session, targetProject.projectId, globalOpts.root) | ||||||
| const newRecord = await provider.copySession(session, targetProject.projectId) | ||||||
|
|
||||||
| // Output success | ||||||
| printSuccessOutput( | ||||||
|
|
||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hardcoded
/separator won't work on Windows - usepath.sepinsteadPrompt To Fix With AI