opencode-manager 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,9 +31,12 @@ Key Features
31
31
  - Missing-only toggle and bulk selection.
32
32
  - Sessions panel
33
33
  - Shows session title (or falls back to ID) in the list.
34
- - Sort toggle (S) between updated and created”.
34
+ - Sort toggle (S) between "updated" and "created".
35
35
  - Each row shows a timestamp snippet for the active sort (created/updated).
36
36
  - Details pane includes title, project ID, updated time, and directory.
37
+ - Rename sessions inline (Shift+R) with validation.
38
+ - Move sessions to another project (M) with project selector.
39
+ - Copy sessions to another project (P) with new session ID generation.
37
40
  - Projects ↔ Sessions workflow
38
41
  - Pressing Enter on a project jumps to the Sessions tab with the project filter set; status text confirms the active filter.
39
42
  - `C` clears the filter (and notifies the user) so global searches go back to all sessions.
@@ -55,13 +58,16 @@ Work Completed
55
58
  - Redesigned Help screen into two columns with color-coded sections and key chips; removed wall-of-text effect.
56
59
  - Added a small color palette and helper components (Section, Row, Bullet, Columns, KeyChip) to simplify consistent styling.
57
60
  - Implemented a global Search bar and input mode:
58
- - “/” to start, Enter to apply, Esc or X to clear.
61
+ - "/" to start, Enter to apply, Esc or "X" to clear.
59
62
  - Projects: search `projectId` and `worktree`; Sessions: search `title`, `sessionId`, `directory`, `projectId`.
60
63
  - Sessions sorting & context:
61
- - S toggles sort by `updated`/`created`.
64
+ - "S" toggles sort by `updated`/`created`.
62
65
  - Show per-row description with the relevant timestamp.
63
66
  - Fixed OpenTUI text rendering error by filtering whitespace-only raw text in layout helpers and by removing nested `<text>` nodes around key chips.
64
67
  - Verified via `bun run tui` (tmux socket creation is blocked in this environment, but direct run works).
68
+ - Added session rename feature (Shift+R): inline text input with validation, updates JSON file, refreshes list.
69
+ - Added session move feature (M): select target project, relocate session JSON, update projectID field.
70
+ - Added session copy feature (P): select target project, create new session with generated ID, preserve original.
65
71
 
66
72
  How To Run
67
73
  ----------
@@ -71,7 +77,7 @@ How To Run
71
77
  - Keys:
72
78
  - Global: `Tab`/`1`/`2` switch tabs, `/` search, `X` clear search, `R` reload, `Q` quit, `?` help
73
79
  - Projects: `Space` select, `A` select all, `M` toggle missing, `D` delete, `Enter` view sessions
74
- - Sessions: `Space` select, `S` sort, `D` delete, `C` clear project filter, `Enter` details
80
+ - Sessions: `Space` select, `S` sort, `D` delete, `Y` copy ID, `Shift+R` rename, `M` move, `P` copy, `C` clear filter
75
81
  - Optional tmux usage (when permitted): `tmux new -s opencode-tui 'bun run tui'`
76
82
  - CLI help: `bun run tui -- --help` (or `bunx opencode-manager -- --help`, or `manage_opencode_projects.py -- --help`) prints the built-in usage block with key bindings.
77
83
 
package/README.md CHANGED
@@ -1,3 +1,6 @@
1
+ > **Note:** This is an independent, community-maintained project created by fans of OpenCode. We are not affiliated with SST Corp. or the official OpenCode project. For the official OpenCode CLI, visit [opencode.ai](https://opencode.ai).
2
+
3
+
1
4
  # OpenCode Metadata Manager
2
5
 
3
6
  Terminal UI for inspecting, filtering, and pruning OpenCode metadata stored on disk. The app is written in TypeScript, runs on Bun, and renders with [`@opentui/react`](https://github.com/open-tui/opentui).
@@ -18,9 +21,12 @@ Terminal UI for inspecting, filtering, and pruning OpenCode metadata stored on d
18
21
 
19
22
  ## Features
20
23
  - List both OpenCode projects and sessions from a local metadata root.
21
- - Filter by missing only”, bulk-select, and delete metadata safely.
24
+ - Filter by "missing only", bulk-select, and delete metadata safely.
22
25
  - Jump from a project directly to its sessions and keep contextual filters.
23
26
  - Global search bar (`/` to focus, `Enter` to apply, `Esc` or `X` to clear).
27
+ - Rename sessions inline (`Shift+R`) with title validation.
28
+ - Move sessions between projects (`M`) preserving session ID.
29
+ - Copy sessions to other projects (`P`) with new session ID generation.
24
30
  - Rich help overlay with live key hints (`?` or `H`).
25
31
  - Zero-install via `bunx` so even CI shells can run it without cloning.
26
32
 
@@ -56,7 +62,7 @@ bun run tui -- --root ~/.local/share/opencode
56
62
  Keyboard reference:
57
63
  - **Global**: `Tab`/`1`/`2` switch tabs, `/` search, `X` clear search, `R` reload, `Q` quit, `?` help.
58
64
  - **Projects**: `Space` toggle selection, `A` select all, `M` missing-only filter, `D` delete, `Enter` jump to Sessions.
59
- - **Sessions**: `Space` select, `S` toggle updated/created sort, `D` delete, `C` clear project filter, `Enter` details.
65
+ - **Sessions**: `Space` select, `S` toggle sort, `D` delete, `Y` copy ID, `Shift+R` rename, `M` move to project, `P` copy to project, `C` clear filter.
60
66
 
61
67
  ## Development Workflow
62
68
  1. Install dependencies with `bun install`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-manager",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Terminal UI for inspecting OpenCode metadata stores.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
  "bun": ">=1.3.0"
10
10
  },
11
11
  "bin": {
12
- "opencode-manager": "./src/bin/opencode-manager.ts"
12
+ "opencode-manager": "src/bin/opencode-manager.ts"
13
13
  },
14
14
  "files": [
15
15
  "src",
@@ -309,3 +309,140 @@ export function describeSession(record: SessionRecord, options?: { fullPath?: bo
309
309
  export async function ensureDirectory(path: string): Promise<void> {
310
310
  await fs.mkdir(dirname(path), { recursive: true })
311
311
  }
312
+
313
+ export async function updateSessionTitle(filePath: string, newTitle: string): Promise<void> {
314
+ const payload = await readJsonFile<any>(filePath)
315
+ if (!payload) {
316
+ throw new Error(`Session file not found or unreadable: ${filePath}`)
317
+ }
318
+ payload.title = newTitle
319
+ payload.time = payload.time || {}
320
+ payload.time.updated = Date.now()
321
+ await fs.writeFile(filePath, JSON.stringify(payload, null, 2), 'utf8')
322
+ }
323
+
324
+ export async function copySession(
325
+ session: SessionRecord,
326
+ targetProjectId: string,
327
+ root: string = DEFAULT_ROOT
328
+ ): Promise<SessionRecord> {
329
+ const payload = await readJsonFile<any>(session.filePath)
330
+ if (!payload) {
331
+ throw new Error(`Session file not found: ${session.filePath}`)
332
+ }
333
+
334
+ // Generate new session ID
335
+ const newSessionId = `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
336
+
337
+ // Update payload for new session
338
+ payload.id = newSessionId
339
+ payload.projectID = targetProjectId
340
+ payload.time = payload.time || {}
341
+ payload.time.created = Date.now()
342
+ payload.time.updated = Date.now()
343
+
344
+ // Ensure target directory exists
345
+ const targetDir = join(root, 'storage', 'session', targetProjectId)
346
+ await ensureDirectory(join(targetDir, 'dummy'))
347
+
348
+ // Write new session file
349
+ const targetPath = join(targetDir, `${newSessionId}.json`)
350
+ await fs.writeFile(targetPath, JSON.stringify(payload, null, 2), 'utf8')
351
+
352
+ // Return new session record
353
+ return {
354
+ index: 0,
355
+ filePath: targetPath,
356
+ sessionId: newSessionId,
357
+ projectId: targetProjectId,
358
+ directory: session.directory,
359
+ title: session.title,
360
+ version: session.version,
361
+ createdAt: new Date(),
362
+ updatedAt: new Date()
363
+ }
364
+ }
365
+
366
+ export async function moveSession(
367
+ session: SessionRecord,
368
+ targetProjectId: string,
369
+ root: string = DEFAULT_ROOT
370
+ ): Promise<SessionRecord> {
371
+ const payload = await readJsonFile<any>(session.filePath)
372
+ if (!payload) {
373
+ throw new Error(`Session file not found: ${session.filePath}`)
374
+ }
375
+
376
+ payload.projectID = targetProjectId
377
+ payload.time = payload.time || {}
378
+ payload.time.updated = Date.now()
379
+
380
+ // Ensure target directory exists
381
+ const targetDir = join(root, 'storage', 'session', targetProjectId)
382
+ await ensureDirectory(join(targetDir, 'dummy'))
383
+
384
+ // Write to new location
385
+ const targetPath = join(targetDir, `${session.sessionId}.json`)
386
+ await fs.writeFile(targetPath, JSON.stringify(payload, null, 2), 'utf8')
387
+
388
+ // Remove old file
389
+ await fs.unlink(session.filePath)
390
+
391
+ return {
392
+ ...session,
393
+ filePath: targetPath,
394
+ projectId: targetProjectId,
395
+ updatedAt: new Date()
396
+ }
397
+ }
398
+
399
+ export interface BatchOperationResult {
400
+ succeeded: { session: SessionRecord; newRecord: SessionRecord }[]
401
+ failed: { session: SessionRecord; error: string }[]
402
+ }
403
+
404
+ export async function copySessions(
405
+ sessions: SessionRecord[],
406
+ targetProjectId: string,
407
+ root?: string
408
+ ): Promise<BatchOperationResult> {
409
+ const succeeded: BatchOperationResult['succeeded'] = []
410
+ const failed: BatchOperationResult['failed'] = []
411
+
412
+ for (const session of sessions) {
413
+ try {
414
+ const newRecord = await copySession(session, targetProjectId, root)
415
+ succeeded.push({ session, newRecord })
416
+ } catch (error) {
417
+ failed.push({
418
+ session,
419
+ error: error instanceof Error ? error.message : String(error)
420
+ })
421
+ }
422
+ }
423
+
424
+ return { succeeded, failed }
425
+ }
426
+
427
+ export async function moveSessions(
428
+ sessions: SessionRecord[],
429
+ targetProjectId: string,
430
+ root?: string
431
+ ): Promise<BatchOperationResult> {
432
+ const succeeded: BatchOperationResult['succeeded'] = []
433
+ const failed: BatchOperationResult['failed'] = []
434
+
435
+ for (const session of sessions) {
436
+ try {
437
+ const newRecord = await moveSession(session, targetProjectId, root)
438
+ succeeded.push({ session, newRecord })
439
+ } catch (error) {
440
+ failed.push({
441
+ session,
442
+ error: error instanceof Error ? error.message : String(error)
443
+ })
444
+ }
445
+ }
446
+
447
+ return { succeeded, failed }
448
+ }
@@ -11,6 +11,7 @@ import React, {
11
11
  useState,
12
12
  } from "react"
13
13
  import { resolve } from "node:path"
14
+ import { exec } from "node:child_process"
14
15
  import {
15
16
  DEFAULT_ROOT,
16
17
  ProjectRecord,
@@ -23,6 +24,12 @@ import {
23
24
  formatDisplayPath,
24
25
  loadProjectRecords,
25
26
  loadSessionRecords,
27
+ updateSessionTitle,
28
+ copySession,
29
+ moveSession,
30
+ copySessions,
31
+ moveSessions,
32
+ BatchOperationResult,
26
33
  } from "./lib/opencode-data"
27
34
 
28
35
  type TabKey = "projects" | "sessions"
@@ -75,6 +82,18 @@ const PALETTE = {
75
82
  muted: "#9ca3af", // gray
76
83
  } as const
77
84
 
85
+ function copyToClipboard(text: string): void {
86
+ const cmd = process.platform === "darwin" ? "pbcopy" : "xclip -selection clipboard"
87
+ const proc = exec(cmd, (error) => {
88
+ if (error) {
89
+ // We can't easily notify from here without context, but it's a best effort
90
+ console.error("Failed to copy to clipboard:", error)
91
+ }
92
+ })
93
+ proc.stdin?.write(text)
94
+ proc.stdin?.end()
95
+ }
96
+
78
97
  type ChildrenProps = { children: React.ReactNode }
79
98
 
80
99
  const Section = ({ title, children }: { title: string } & ChildrenProps) => (
@@ -125,6 +144,62 @@ const Columns = ({ children }: ChildrenProps) => {
125
144
 
126
145
  const KeyChip = ({ k }: { k: string }) => <text fg={PALETTE.key}>[{k}]</text>
127
146
 
147
+ type ProjectSelectorProps = {
148
+ projects: ProjectRecord[]
149
+ cursor: number
150
+ onCursorChange: (index: number) => void
151
+ onSelect: (project: ProjectRecord) => void
152
+ onCancel: () => void
153
+ operationMode: 'move' | 'copy'
154
+ sessionCount: number
155
+ }
156
+
157
+ const ProjectSelector = ({
158
+ projects,
159
+ cursor,
160
+ onCursorChange,
161
+ onSelect,
162
+ onCancel,
163
+ operationMode,
164
+ sessionCount
165
+ }: ProjectSelectorProps) => {
166
+ const options: SelectOption[] = projects.map((p, idx) => ({
167
+ name: `${formatDisplayPath(p.worktree)} (${p.projectId})`,
168
+ description: p.state,
169
+ value: idx
170
+ }))
171
+
172
+ return (
173
+ <box
174
+ title={`Select Target Project (${operationMode} ${sessionCount} session${sessionCount > 1 ? 's' : ''})`}
175
+ style={{
176
+ border: true,
177
+ borderColor: operationMode === 'move' ? PALETTE.key : PALETTE.accent,
178
+ padding: 1,
179
+ position: 'absolute',
180
+ top: 5,
181
+ left: 5,
182
+ right: 5,
183
+ bottom: 5,
184
+ zIndex: 100
185
+ }}
186
+ >
187
+ <select
188
+ options={options}
189
+ selectedIndex={cursor}
190
+ onChange={onCursorChange}
191
+ onSelect={(idx) => {
192
+ const project = projects[idx]
193
+ if (project) onSelect(project)
194
+ }}
195
+ focused={true}
196
+ showScrollIndicator
197
+ />
198
+ <text fg={PALETTE.muted}>Enter to select, Esc to cancel</text>
199
+ </box>
200
+ )
201
+ }
202
+
128
203
  const ProjectsPanel = forwardRef<PanelHandle, ProjectsPanelProps>(function ProjectsPanel(
129
204
  { root, active, locked, searchQuery, onNotify, requestConfirm, onNavigateToSessions },
130
205
  ref,
@@ -390,6 +465,12 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
390
465
  const [cursor, setCursor] = useState(0)
391
466
  const [selectedIndexes, setSelectedIndexes] = useState<Set<number>>(new Set())
392
467
  const [sortMode, setSortMode] = useState<"updated" | "created">("updated")
468
+ const [isRenaming, setIsRenaming] = useState(false)
469
+ const [renameValue, setRenameValue] = useState('')
470
+ const [isSelectingProject, setIsSelectingProject] = useState(false)
471
+ const [operationMode, setOperationMode] = useState<'move' | 'copy' | null>(null)
472
+ const [availableProjects, setAvailableProjects] = useState<ProjectRecord[]>([])
473
+ const [projectCursor, setProjectCursor] = useState(0)
393
474
 
394
475
  const visibleRecords = useMemo(() => {
395
476
  const sorted = [...records].sort((a, b) => {
@@ -527,12 +608,103 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
527
608
  })
528
609
  }, [selectedSessions, onNotify, requestConfirm, refreshRecords])
529
610
 
611
+ const executeRename = useCallback(async () => {
612
+ if (!currentSession || !renameValue.trim()) {
613
+ onNotify('Title cannot be empty', 'error')
614
+ setIsRenaming(false)
615
+ return
616
+ }
617
+ if (renameValue.length > 200) {
618
+ onNotify('Title too long (max 200 characters)', 'error')
619
+ return
620
+ }
621
+ try {
622
+ await updateSessionTitle(currentSession.filePath, renameValue.trim())
623
+ onNotify(`Renamed to "${renameValue.trim()}"`)
624
+ setIsRenaming(false)
625
+ setRenameValue('')
626
+ await refreshRecords(true)
627
+ } catch (error) {
628
+ const msg = error instanceof Error ? error.message : String(error)
629
+ onNotify(`Rename failed: ${msg}`, 'error')
630
+ }
631
+ }, [currentSession, renameValue, onNotify, refreshRecords])
632
+
633
+ const executeTransfer = useCallback(async (
634
+ targetProject: ProjectRecord,
635
+ mode: 'move' | 'copy'
636
+ ) => {
637
+ setIsSelectingProject(false)
638
+ setOperationMode(null)
639
+
640
+ const operationFn = mode === 'move' ? moveSessions : copySessions
641
+ const result = await operationFn(selectedSessions, targetProject.projectId, root)
642
+
643
+ setSelectedIndexes(new Set())
644
+
645
+ const successCount = result.succeeded.length
646
+ const failCount = result.failed.length
647
+ const verb = mode === 'move' ? 'moved' : 'copied'
648
+
649
+ if (failCount === 0) {
650
+ onNotify(`Successfully ${verb} ${successCount} session(s) to ${targetProject.projectId}`)
651
+ } else {
652
+ onNotify(
653
+ `${verb} ${successCount} session(s), ${failCount} failed`,
654
+ 'error'
655
+ )
656
+ }
657
+
658
+ await refreshRecords(true)
659
+ }, [selectedSessions, root, onNotify, refreshRecords])
660
+
530
661
  const handleKey = useCallback(
531
662
  (key: KeyEvent) => {
532
663
  if (!active || locked) {
533
664
  return
534
665
  }
535
666
 
667
+ // Handle project selection mode
668
+ if (isSelectingProject) {
669
+ if (key.name === 'escape') {
670
+ setIsSelectingProject(false)
671
+ setOperationMode(null)
672
+ return
673
+ }
674
+ if (key.name === 'return' || key.name === 'enter') {
675
+ const targetProject = availableProjects[projectCursor]
676
+ if (targetProject && operationMode) {
677
+ void executeTransfer(targetProject, operationMode)
678
+ }
679
+ return
680
+ }
681
+ // Let select component handle up/down via onCursorChange
682
+ return
683
+ }
684
+
685
+ // Handle rename mode - takes precedence over other key handling
686
+ if (isRenaming) {
687
+ if (key.name === 'escape') {
688
+ setIsRenaming(false)
689
+ setRenameValue('')
690
+ return
691
+ }
692
+ if (key.name === 'return' || key.name === 'enter') {
693
+ void executeRename()
694
+ return
695
+ }
696
+ if (key.name === 'backspace') {
697
+ setRenameValue(prev => prev.slice(0, -1))
698
+ return
699
+ }
700
+ const ch = key.sequence
701
+ if (ch && ch.length === 1 && !key.ctrl && !key.meta) {
702
+ setRenameValue(prev => prev + ch)
703
+ return
704
+ }
705
+ return
706
+ }
707
+
536
708
  const letter = key.sequence?.toLowerCase()
537
709
  if (key.name === "space") {
538
710
  key.preventDefault()
@@ -555,6 +727,58 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
555
727
  requestDeletion()
556
728
  return
557
729
  }
730
+ if (letter === "y") {
731
+ if (currentSession) {
732
+ copyToClipboard(currentSession.sessionId)
733
+ onNotify(`Copied ID ${currentSession.sessionId} to clipboard`)
734
+ }
735
+ return
736
+ }
737
+ // Rename with Shift+R (uppercase R)
738
+ if (key.sequence === 'R') {
739
+ if (currentSession) {
740
+ setIsRenaming(true)
741
+ setRenameValue(currentSession.title || '')
742
+ }
743
+ return
744
+ }
745
+ // Move with M key
746
+ if (letter === 'm') {
747
+ if (selectedSessions.length === 0) {
748
+ onNotify('No sessions selected for move', 'error')
749
+ return
750
+ }
751
+ // Load projects for selection
752
+ loadProjectRecords({ root }).then(projects => {
753
+ // Filter out current project if filtering by project
754
+ const filtered = projectFilter
755
+ ? projects.filter(p => p.projectId !== projectFilter)
756
+ : projects
757
+ setAvailableProjects(filtered)
758
+ setProjectCursor(0)
759
+ setOperationMode('move')
760
+ setIsSelectingProject(true)
761
+ }).catch(err => {
762
+ onNotify(`Failed to load projects: ${err.message}`, 'error')
763
+ })
764
+ return
765
+ }
766
+ // Copy with P key
767
+ if (letter === 'p') {
768
+ if (selectedSessions.length === 0) {
769
+ onNotify('No sessions selected for copy', 'error')
770
+ return
771
+ }
772
+ loadProjectRecords({ root }).then(projects => {
773
+ setAvailableProjects(projects)
774
+ setProjectCursor(0)
775
+ setOperationMode('copy')
776
+ setIsSelectingProject(true)
777
+ }).catch(err => {
778
+ onNotify(`Failed to load projects: ${err.message}`, 'error')
779
+ })
780
+ return
781
+ }
558
782
  if (key.name === "return" || key.name === "enter") {
559
783
  if (currentSession) {
560
784
  const title = currentSession.title && currentSession.title.trim().length > 0 ? currentSession.title : currentSession.sessionId
@@ -563,7 +787,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
563
787
  return
564
788
  }
565
789
  },
566
- [active, locked, currentSession, projectFilter, onClearFilter, onNotify, requestDeletion, toggleSelection],
790
+ [active, locked, currentSession, projectFilter, onClearFilter, onNotify, requestDeletion, toggleSelection, isRenaming, executeRename, isSelectingProject, availableProjects, projectCursor, operationMode, executeTransfer, selectedSessions, root],
567
791
  )
568
792
 
569
793
  useImperativeHandle(
@@ -590,9 +814,32 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
590
814
  >
591
815
  <box flexDirection="column" marginBottom={1}>
592
816
  <text>Filter: {projectFilter ? `project ${projectFilter}` : "none"} | Sort: {sortMode} | Search: {searchQuery || "(none)"} | Selected: {selectedIndexes.size}</text>
593
- <text>Keys: Space select, S sort, D delete, C clear filter, Enter details, Esc clear</text>
817
+ <text>Keys: Space select, S sort, D delete, Y copy ID, Shift+R rename, M move, P copy, C clear filter</text>
594
818
  </box>
595
819
 
820
+ {isRenaming ? (
821
+ <box style={{ border: true, borderColor: PALETTE.key, padding: 1, marginBottom: 1 }}>
822
+ <text>Rename: </text>
823
+ <text fg={PALETTE.key}>{renameValue}</text>
824
+ <text fg={PALETTE.muted}> (Enter confirm, Esc cancel)</text>
825
+ </box>
826
+ ) : null}
827
+
828
+ {isSelectingProject && operationMode ? (
829
+ <ProjectSelector
830
+ projects={availableProjects}
831
+ cursor={projectCursor}
832
+ onCursorChange={setProjectCursor}
833
+ onSelect={(project) => executeTransfer(project, operationMode)}
834
+ onCancel={() => {
835
+ setIsSelectingProject(false)
836
+ setOperationMode(null)
837
+ }}
838
+ operationMode={operationMode}
839
+ sessionCount={selectedSessions.length}
840
+ />
841
+ ) : null}
842
+
596
843
  {error ? (
597
844
  <text fg="red">{error}</text>
598
845
  ) : loading ? (
@@ -613,7 +860,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
613
860
  onNotify(`Session ${title} [${session.sessionId}] → ${formatDisplayPath(session.directory)}`)
614
861
  }
615
862
  }}
616
- focused={active && !locked}
863
+ focused={active && !locked && !isSelectingProject && !isRenaming}
617
864
  showScrollIndicator
618
865
  showDescription={false}
619
866
  wrapSelection={false}
@@ -628,6 +875,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
628
875
  <text>Updated: {formatDate(currentSession.updatedAt || currentSession.createdAt)}</text>
629
876
  <text>Directory:</text>
630
877
  <text>{formatDisplayPath(currentSession.directory, { fullPath: true })}</text>
878
+ <text fg={PALETTE.muted} style={{ marginTop: 1 }}>Press Y to copy ID</text>
631
879
  </box>
632
880
  ) : null}
633
881
  </box>
@@ -755,6 +1003,22 @@ const HelpScreen = ({ onDismiss }: { onDismiss: () => void }) => {
755
1003
  <KeyChip k="D" />
756
1004
  <text> — With confirmation</text>
757
1005
  </Bullet>
1006
+ <Bullet>
1007
+ <text>Copy ID: </text>
1008
+ <KeyChip k="Y" />
1009
+ </Bullet>
1010
+ <Bullet>
1011
+ <text>Rename: </text>
1012
+ <KeyChip k="Shift+R" />
1013
+ </Bullet>
1014
+ <Bullet>
1015
+ <text>Move to project: </text>
1016
+ <KeyChip k="M" />
1017
+ </Bullet>
1018
+ <Bullet>
1019
+ <text>Copy to project: </text>
1020
+ <KeyChip k="P" />
1021
+ </Bullet>
758
1022
  <Bullet>
759
1023
  <text>Show details: </text>
760
1024
  <KeyChip k="Enter" />