mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-08-21 06:13:26 +00:00
refactor(v2): remove dead migration scaffolding
Delete the obsolete pre-MVP roadmap and task references now that Git preserves that development history, while retaining current V2 operational and architecture guidance. Remove behavior-neutral server and UI plumbing that has no remaining caller: unused workspace options and cached endpoint state, redundant auto-accept metadata, an unused PTY get adapter, and a one-use message-loading helper. Keep lifecycle proof, restore gates, timeline projection, and targeted invalidation because they still enforce runtime behavior. Validated with server and UI typechecks, the full server suite (308 passed, 3 skipped), focused UI matrices, browser-conditioned UI tests, production server/UI build, diff checks, caller searches, and an independent regression review.
This commit is contained in:
parent
a820f80be3
commit
775c6c6ca1
23 changed files with 38 additions and 786 deletions
1
.github/workflows/pr-build.yml
vendored
1
.github/workflows/pr-build.yml
vendored
|
|
@ -104,7 +104,6 @@ jobs:
|
|||
- name: Test changed runnable UI behavior
|
||||
run: >-
|
||||
node --import tsx --test
|
||||
packages/ui/src/components/message-loading-visibility.test.ts
|
||||
packages/ui/src/components/message-timeline-v2.test.ts
|
||||
packages/ui/src/components/provider-auth/provider-options.test.ts
|
||||
packages/ui/src/components/session/session-bottom-pin-intent.test.ts
|
||||
|
|
|
|||
|
|
@ -51,14 +51,6 @@ The migration removes the V1 compatibility layer rather than maintaining both in
|
|||
- Force the V2 service database to `~/.local/share/opencode2/opencode.db`. V1 and V2 must never point at the same database because their schemas are incompatible.
|
||||
- Isolate V2 restore state under `~/.codenomad/client-state/v2` and copy V1 state non-destructively on first launch, preserving downgrade history.
|
||||
|
||||
## Expected Benefits
|
||||
|
||||
- Less custom integration code and fewer long-running processes.
|
||||
- Closer alignment with the supported OpenCode V2 architecture.
|
||||
- A smaller native integration surface without maintaining V1 compatibility code.
|
||||
- Consistent behavior between root workspaces and Git worktrees.
|
||||
- Simpler service startup, event handling, and client-side API access.
|
||||
|
||||
## Current Status
|
||||
|
||||
- The server/UI client dependencies are aligned on the latest reviewed OpenCode `next` release. The installed `opencode2` CLI is not exact-version-gated at runtime.
|
||||
|
|
|
|||
|
|
@ -57,41 +57,6 @@ Executive summary of the entire project - **start here!**
|
|||
|
||||
**Read this to understand:** Current implementation boundaries
|
||||
|
||||
### [build-roadmap.md](build-roadmap.md)
|
||||
|
||||
**Development plan**
|
||||
|
||||
- 8 phases of development
|
||||
- Task dependencies
|
||||
- Timeline estimates
|
||||
- Success criteria
|
||||
- Risk mitigation
|
||||
|
||||
**Read this to understand:** The development journey from start to finish
|
||||
|
||||
---
|
||||
|
||||
## Task Documents
|
||||
|
||||
### [tasks/README.md](../tasks/README.md)
|
||||
|
||||
**Task management guide**
|
||||
|
||||
- Task workflow
|
||||
- Naming conventions
|
||||
- How to work on tasks
|
||||
- Progress tracking
|
||||
|
||||
### Task Files (in tasks/todo/)
|
||||
|
||||
- **001-project-setup.md** - Electron + SolidJS boilerplate
|
||||
- **002-empty-state-ui.md** - Initial UI with folder selection
|
||||
- **003-process-manager.md** - OpenCode server spawning
|
||||
- **004-sdk-integration.md** - API client integration
|
||||
- **005-session-picker-modal.md** - Session selection UI
|
||||
|
||||
More tasks will be added as we progress through phases.
|
||||
|
||||
---
|
||||
|
||||
## Reading Order
|
||||
|
|
@ -101,15 +66,11 @@ More tasks will be added as we progress through phases.
|
|||
1. [SUMMARY.md](SUMMARY.md) - Get the big picture
|
||||
2. [architecture.md](architecture.md) - Understand the structure
|
||||
3. [user-interface.md](user-interface.md) - See what you're building
|
||||
4. [build-roadmap.md](build-roadmap.md) - Understand the plan
|
||||
5. [tasks/README.md](../tasks/README.md) - Learn the workflow
|
||||
|
||||
### For Implementers:
|
||||
|
||||
1. [tasks/README.md](../tasks/README.md) - Understand task workflow
|
||||
2. [technical-implementation.md](technical-implementation.md) - Implementation patterns
|
||||
3. [tasks/todo/001-\*.md](../tasks/todo/) - Start with first task
|
||||
4. Refer to architecture.md and user-interface.md as needed
|
||||
1. [technical-implementation.md](technical-implementation.md) - Implementation patterns
|
||||
2. Refer to architecture.md and user-interface.md as needed
|
||||
|
||||
### For Designers:
|
||||
|
||||
|
|
@ -117,12 +78,6 @@ More tasks will be added as we progress through phases.
|
|||
2. [architecture.md](architecture.md) - Component structure
|
||||
3. [SUMMARY.md](SUMMARY.md) - Feature overview
|
||||
|
||||
### For Project Managers:
|
||||
|
||||
1. [SUMMARY.md](SUMMARY.md) - Executive overview
|
||||
2. [build-roadmap.md](build-roadmap.md) - Timeline and phases
|
||||
3. [tasks/README.md](../tasks/README.md) - Task tracking
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
|
@ -130,10 +85,7 @@ More tasks will be added as we progress through phases.
|
|||
### Common Questions
|
||||
|
||||
**Q: Where do I start?**
|
||||
A: Read [SUMMARY.md](SUMMARY.md), then start [Task 001](../tasks/todo/001-project-setup.md)
|
||||
|
||||
**Q: How long will this take?**
|
||||
A: See [build-roadmap.md](build-roadmap.md) - MVP in 3-7 weeks depending on commitment
|
||||
A: Read [SUMMARY.md](SUMMARY.md), then [architecture.md](architecture.md) and [technical-implementation.md](technical-implementation.md).
|
||||
|
||||
**Q: What does the UI look like?**
|
||||
A: See [user-interface.md](user-interface.md) for complete specifications
|
||||
|
|
@ -143,38 +95,3 @@ A: See [architecture.md](architecture.md) for system design
|
|||
|
||||
**Q: How do I build feature X?**
|
||||
A: See [technical-implementation.md](technical-implementation.md) for patterns
|
||||
|
||||
**Q: What's the development plan?**
|
||||
A: See [build-roadmap.md](build-roadmap.md) for phases
|
||||
|
||||
---
|
||||
|
||||
## Document Status
|
||||
|
||||
| Document | Status | Last Updated |
|
||||
| --------------------------- | ----------- | ------------ |
|
||||
| README.md | ✅ Complete | 2024-10-22 |
|
||||
| SUMMARY.md | ✅ Complete | 2024-10-22 |
|
||||
| architecture.md | ✅ Complete | 2024-10-22 |
|
||||
| user-interface.md | ✅ Complete | 2024-10-22 |
|
||||
| technical-implementation.md | ✅ Complete | 2024-10-22 |
|
||||
| build-roadmap.md | ✅ Complete | 2024-10-22 |
|
||||
| tasks/README.md | ✅ Complete | 2024-10-22 |
|
||||
| Task 001-005 | ✅ Complete | 2024-10-22 |
|
||||
|
||||
**Project phase:** Post-MVP (Phases 1-3 complete; Phase 4 work underway).
|
||||
|
||||
---
|
||||
|
||||
## Contributing to Documentation
|
||||
|
||||
When updating documentation:
|
||||
|
||||
1. Update the relevant file
|
||||
2. Update "Last Updated" in this index
|
||||
3. Update SUMMARY.md if adding major changes
|
||||
4. Keep consistent formatting and style
|
||||
|
||||
---
|
||||
|
||||
_This index will be updated as more documentation is added._
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
## Current Status
|
||||
|
||||
We have completed the MVP milestones (Phases 1-3) and are now operating in post-MVP mode. Future work prioritizes multi-instance support, advanced input polish, and system integrations outlined in later phases.
|
||||
The MVP and multi-instance milestones are complete. Current architecture and implementation details live in the documents indexed below.
|
||||
|
||||
## What We've Created
|
||||
|
||||
A comprehensive specification and task breakdown for building the CodeNomad desktop application.
|
||||
Development documentation for the CodeNomad desktop application.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
|
|
@ -16,7 +16,6 @@ packages/ui/ SolidJS UI and native Promise clients
|
|||
packages/electron-app Electron host
|
||||
packages/tauri-app/ Tauri host
|
||||
dev-docs/ Development documentation
|
||||
tasks/ Task tracking
|
||||
```
|
||||
|
||||
## Documentation Overview
|
||||
|
|
@ -85,70 +84,6 @@ tasks/ Task tracking
|
|||
- Message rendering implementation
|
||||
- Build and packaging config
|
||||
|
||||
### 4. Build Roadmap (build-roadmap.md)
|
||||
|
||||
**What it covers:**
|
||||
|
||||
- 8 development phases
|
||||
- Task dependencies
|
||||
- Timeline estimates
|
||||
- Success criteria per phase
|
||||
- Risk mitigation
|
||||
- Release strategy
|
||||
|
||||
**Phases:**
|
||||
|
||||
1. **Foundation** (Week 1) - Project setup, process management
|
||||
2. **Core Chat** (Week 2) - Message display, SSE streaming
|
||||
3. **Essential Features** (Week 3) - Markdown, agents, errors
|
||||
4. **Multi-Instance** (Week 4) - Multiple projects support
|
||||
5. **Advanced Input** (Week 5) - Commands, file attachments
|
||||
6. **Polish** (Week 6) - UX refinements, settings
|
||||
7. **System Integration** (Week 7) - Native features
|
||||
8. **Advanced** (Week 8+) - Performance, plugins
|
||||
|
||||
## Task Breakdown
|
||||
|
||||
### Current Tasks (Phase 1)
|
||||
|
||||
**001 - Project Setup** (2-3 hours)
|
||||
|
||||
- Set up Electron + SolidJS + Vite
|
||||
- Configure TypeScript, TailwindCSS
|
||||
- Create basic project structure
|
||||
- Verify build pipeline works
|
||||
|
||||
**002 - Empty State UI** (2-3 hours)
|
||||
|
||||
- Create empty state component
|
||||
- Implement folder selection dialog
|
||||
- Add keyboard shortcuts
|
||||
- Style and test responsiveness
|
||||
|
||||
**003 - Shared Service Manager** (4-5 hours)
|
||||
|
||||
- Discover or launch one OpenCode service through CodeNomad's lease-locked process-proof lifecycle
|
||||
- Validate workspace locations/directories
|
||||
- Transfer proof to a live peer or stop only the exact proven daemon on final shutdown
|
||||
- Handle errors and timeouts
|
||||
- Auto-cleanup on app quit
|
||||
|
||||
**004 - Native Client Integration** (3-4 hours)
|
||||
|
||||
- Create native clients through the CodeNomad proxy
|
||||
- Fetch sessions, agents, models
|
||||
- Implement session CRUD operations
|
||||
- Add error handling and retries
|
||||
|
||||
**005 - Session Picker Modal** (3-4 hours)
|
||||
|
||||
- Build modal with session list
|
||||
- Agent selector for new sessions
|
||||
- Keyboard navigation
|
||||
- Loading and error states
|
||||
|
||||
**Total Phase 1 time: ~15-20 hours (2-3 weeks part-time)**
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### 1. Two-Level Tabs
|
||||
|
|
@ -194,15 +129,6 @@ tasks/ Task tracking
|
|||
|
||||
## Implementation Guidelines
|
||||
|
||||
### For Each Task:
|
||||
|
||||
1. Read task file completely
|
||||
2. Review related documentation
|
||||
3. Follow steps in order
|
||||
4. Check off acceptance criteria
|
||||
5. Test thoroughly
|
||||
6. Move to done/ when complete
|
||||
|
||||
### Code Standards:
|
||||
|
||||
- TypeScript for everything
|
||||
|
|
@ -220,73 +146,14 @@ tasks/ Task tracking
|
|||
- Test edge cases (long text, special chars)
|
||||
- Keyboard navigation verification
|
||||
|
||||
## Next Steps
|
||||
|
||||
### To Start Building:
|
||||
|
||||
1. **Read all documentation**
|
||||
- Understand architecture
|
||||
- Review UI specifications
|
||||
- Study technical approach
|
||||
|
||||
2. **Start with Task 001**
|
||||
- Set up project structure
|
||||
- Install dependencies
|
||||
- Verify build works
|
||||
|
||||
3. **Follow sequential order**
|
||||
- Each task builds on previous
|
||||
- Don't skip ahead
|
||||
- Dependencies matter
|
||||
|
||||
4. **Track progress**
|
||||
- Update task checkboxes
|
||||
- Move completed tasks to done/
|
||||
- Update roadmap as you go
|
||||
|
||||
### When You Hit Issues:
|
||||
|
||||
1. Review task prerequisites
|
||||
2. Check documentation for clarification
|
||||
3. Look at related specs
|
||||
4. Ask questions on unclear requirements
|
||||
5. Document blockers and solutions
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### MVP (After Task 015)
|
||||
|
||||
- Can select folder → spawn server → chat
|
||||
- Messages stream in real-time
|
||||
- Can switch agents and models
|
||||
- Tool executions visible
|
||||
- Basic error handling works
|
||||
- **Performance is NOT a concern** - focus on functionality
|
||||
|
||||
### Beta (After Task 030)
|
||||
|
||||
- Multi-instance support
|
||||
- Advanced input (files, commands)
|
||||
- Polished UX
|
||||
- Settings and preferences
|
||||
- Native menus
|
||||
|
||||
### v1.0 (After Task 035)
|
||||
|
||||
- System tray integration
|
||||
- Auto-updates
|
||||
- Crash reporting
|
||||
- Production-ready stability
|
||||
|
||||
## Useful References
|
||||
|
||||
### Within This Project:
|
||||
|
||||
- `README.md` - Project overview and getting started
|
||||
- `docs/architecture.md` - System design
|
||||
- `docs/user-interface.md` - UI specifications
|
||||
- `docs/technical-implementation.md` - Implementation details
|
||||
- `tasks/README.md` - Task workflow guide
|
||||
- `dev-docs/architecture.md` - System design
|
||||
- `dev-docs/user-interface.md` - UI specifications
|
||||
- `dev-docs/technical-implementation.md` - Implementation details
|
||||
|
||||
### External:
|
||||
|
||||
|
|
@ -307,33 +174,3 @@ tasks/ Task tracking
|
|||
- PTYs: location-scoped native entries in Status, refreshed on PTY events/reconnect with metadata, title updates, and ownership-checked removal; current installed declarations have no output/read/stream or separate stop API, so output and distinct stop are unavailable and removal stops a running PTY
|
||||
- Legacy plugin/background processes: `packages/opencode-plugin` and server plugin/background-process paths remain deleted
|
||||
- Git mutations and Yolo: CodeNomad-owned
|
||||
|
||||
## Estimated Timeline
|
||||
|
||||
**Conservative estimate (part-time, ~15 hours/week):**
|
||||
|
||||
- Phase 1 (MVP Foundation): 2-3 weeks
|
||||
- Phase 2 (Core Chat): 2 weeks
|
||||
- Phase 3 (Essential): 2 weeks
|
||||
- **MVP Complete: 6-7 weeks**
|
||||
|
||||
**Aggressive estimate (full-time, ~40 hours/week):**
|
||||
|
||||
- Phase 1: 1 week
|
||||
- Phase 2: 1 week
|
||||
- Phase 3: 1 week
|
||||
- **MVP Complete: 3 weeks**
|
||||
|
||||
Add 2-4 weeks for testing, bug fixes, and polish before alpha release.
|
||||
|
||||
## This is a Living Document
|
||||
|
||||
As you build:
|
||||
|
||||
- Update estimates based on actual time
|
||||
- Add new tasks as needed
|
||||
- Refine specifications
|
||||
- Document learnings
|
||||
- Track blockers and solutions
|
||||
|
||||
Good luck! 🚀
|
||||
|
|
|
|||
|
|
@ -1,391 +0,0 @@
|
|||
# CodeNomad Build Roadmap
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the phased approach to building the CodeNomad desktop application. Each phase builds incrementally on the previous, with clear deliverables and milestones.
|
||||
|
||||
**Status:** MVP (Phases 1-3) is complete. Focus now shifts to post-MVP phases starting with multi-instance support and advanced input refinements.
|
||||
|
||||
## MVP Scope (Phases 1-3)
|
||||
|
||||
The minimum viable product includes:
|
||||
|
||||
- Single instance management
|
||||
- Session selection and creation
|
||||
- Message display (streaming)
|
||||
- Basic prompt input (text only)
|
||||
- Agent/model selection
|
||||
- Process lifecycle management
|
||||
|
||||
**Target: 3-4 weeks for MVP**
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation (Week 1)
|
||||
|
||||
**Goal:** Running desktop app connected to one shared OpenCode service
|
||||
|
||||
### Tasks
|
||||
|
||||
1. ✅ **001-project-setup** - Electron + SolidJS + Vite boilerplate
|
||||
2. ✅ **002-empty-state-ui** - Empty state UI with folder selection
|
||||
3. ✅ **003-process-manager** - Discover/start and manage the shared OpenCode service
|
||||
4. ✅ **004-sdk-integration** - Connect through the native OpenCode client
|
||||
5. ✅ **005-session-picker-modal** - Select/create session modal
|
||||
|
||||
### Deliverables
|
||||
|
||||
- App launches successfully
|
||||
- Can select folder
|
||||
- Shared service starts or reconnects automatically
|
||||
- Session picker appears
|
||||
- Can create/select session
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- User can launch app → select folder → see session picker
|
||||
- Workspace location is ready on the shared service
|
||||
- Sessions fetch from API successfully
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Core Chat Interface (Week 2)
|
||||
|
||||
**Goal:** Display messages and send basic prompts
|
||||
|
||||
### Tasks
|
||||
|
||||
6. **006-instance-session-tabs** - Two-level tab navigation
|
||||
7. **007-message-display** - Render user and assistant messages
|
||||
8. **008-sse-integration** - Real-time message streaming
|
||||
9. **009-prompt-input-basic** - Text input with send functionality
|
||||
10. **010-tool-call-rendering** - Display tool executions inline
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Tab navigation works
|
||||
- Messages display correctly
|
||||
- Real-time updates via SSE
|
||||
- Can send text messages
|
||||
- Tool calls show status
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- User can type message → see response stream in real-time
|
||||
- Tool executions visible and expandable
|
||||
- Multiple sessions can be open simultaneously
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Essential Features (Week 3)
|
||||
|
||||
**Goal:** Feature parity with basic TUI functionality
|
||||
|
||||
### Tasks
|
||||
|
||||
11. **011-agent-model-selectors** - Dropdown for agent/model switching
|
||||
12. **012-markdown-rendering** - Proper markdown with code highlighting
|
||||
13. **013-logs-tab** - View server logs
|
||||
14. **014-error-handling** - Comprehensive error states and recovery
|
||||
15. **015-keyboard-shortcuts** - Essential keyboard navigation
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Can switch agents and models
|
||||
- Markdown renders beautifully
|
||||
- Code blocks have syntax highlighting
|
||||
- Server logs accessible
|
||||
- Errors handled gracefully
|
||||
- Cmd/Ctrl+N, K, L shortcuts work
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- User experience matches TUI quality
|
||||
- All error cases handled
|
||||
- Keyboard-first navigation option available
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Multi-Instance Support (Week 4)
|
||||
|
||||
**Goal:** Work on multiple projects simultaneously
|
||||
|
||||
### Tasks
|
||||
|
||||
16. **016-instance-tabs** - Instance-level tab management
|
||||
17. **017-instance-state-persistence** - Remember instances across restarts
|
||||
18. **018-child-session-handling** - Auto-create tabs for child sessions
|
||||
19. **019-instance-lifecycle** - Stop, restart, reconnect instances
|
||||
20. **020-multiple-sdk-clients** - Location-scoped clients over one shared service
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Multiple instance tabs
|
||||
- Persists across app restarts
|
||||
- Child sessions appear as new tabs
|
||||
- Can stop individual instances
|
||||
- All instances work independently
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- User can work on 3+ projects simultaneously
|
||||
- App remembers state on restart
|
||||
- No interference between instances
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Advanced Input (Week 5)
|
||||
|
||||
**Goal:** Full input capabilities matching TUI
|
||||
|
||||
### Tasks
|
||||
|
||||
21. **021-slash-commands** - Command palette with autocomplete
|
||||
22. **022-file-attachments** - @ mention file picker
|
||||
23. **023-drag-drop-files** - Drag files onto input
|
||||
24. **024-attachment-chips** - Display and manage attachments
|
||||
25. **025-input-history** - Up/down arrow message history
|
||||
|
||||
### Deliverables
|
||||
|
||||
- `/command` autocomplete works
|
||||
- `@file` picker searches files
|
||||
- Drag & drop attaches files
|
||||
- Attachment chips removable
|
||||
- Previous messages accessible
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- Input feature parity with TUI
|
||||
- File context easy to add
|
||||
- Command discovery intuitive
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & UX (Week 6)
|
||||
|
||||
**Goal:** Production-ready user experience
|
||||
|
||||
### Tasks
|
||||
|
||||
26. **026-message-actions** - Copy, edit, regenerate messages
|
||||
27. **027-search-in-session** - Find text in conversation
|
||||
28. **028-session-management** - Rename, share, export sessions
|
||||
29. **029-settings-ui** - Preferences and configuration
|
||||
30. **030-native-menus** - Platform-native menu bar
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Message context menus
|
||||
- Search within conversation
|
||||
- Session CRUD operations
|
||||
- Settings dialog
|
||||
- Native File/Edit/View menus
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- Feels polished and professional
|
||||
- All common actions accessible
|
||||
- Settings discoverable
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: System Integration (Week 7)
|
||||
|
||||
**Goal:** Native desktop app features
|
||||
|
||||
### Tasks
|
||||
|
||||
31. **031-system-tray** - Background running with tray icon
|
||||
32. **032-notifications** - Desktop notifications for events
|
||||
33. **033-auto-updater** - In-app update mechanism
|
||||
34. **034-crash-reporting** - Error reporting and recovery
|
||||
35. **035-performance-profiling** - Optimize rendering and memory
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Runs in background
|
||||
- Notifications for session activity
|
||||
- Auto-updates on launch
|
||||
- Crash logs captured
|
||||
- Smooth performance with large sessions
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- App feels native to platform
|
||||
- Updates seamlessly
|
||||
- Crashes don't lose data
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Advanced Features (Week 8+)
|
||||
|
||||
**Goal:** Beyond MVP, power user features
|
||||
|
||||
### Tasks
|
||||
|
||||
36. **036-virtual-scrolling** - Handle 1000+ message sessions
|
||||
37. **037-message-search-advanced** - Full-text search across sessions
|
||||
38. **038-workspace-management** - Save/load workspace configurations
|
||||
39. **039-theme-customization** - Custom themes and UI tweaks
|
||||
40. **040-native-capabilities** - Integrate additional native OpenCode capabilities
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Virtual scrolling for performance
|
||||
- Cross-session search
|
||||
- Workspace persistence
|
||||
- Theme editor
|
||||
- Native capability integration
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- Handles massive sessions (5000+ messages)
|
||||
- Can search entire project history
|
||||
- Fully customizable
|
||||
|
||||
---
|
||||
|
||||
## Parallel Tracks
|
||||
|
||||
Some tasks can be worked on independently:
|
||||
|
||||
### Design Track
|
||||
|
||||
- Visual design refinements
|
||||
- Icon creation
|
||||
- Brand assets
|
||||
- Marketing materials
|
||||
|
||||
### Documentation Track
|
||||
|
||||
- User guide
|
||||
- Keyboard shortcuts reference
|
||||
- Troubleshooting docs
|
||||
- Video tutorials
|
||||
|
||||
### Infrastructure Track
|
||||
|
||||
- CI/CD pipeline
|
||||
- Automated testing
|
||||
- Release automation
|
||||
- Analytics integration
|
||||
|
||||
---
|
||||
|
||||
## Release Strategy
|
||||
|
||||
### Alpha (After Phase 3)
|
||||
|
||||
- Internal testing only
|
||||
- Frequent bugs expected
|
||||
- Rapid iteration
|
||||
|
||||
### Beta (After Phase 6)
|
||||
|
||||
- Public beta program
|
||||
- Feature complete
|
||||
- Bug fixes and polish
|
||||
|
||||
### v1.0 (After Phase 7)
|
||||
|
||||
- Public release
|
||||
- Stable and reliable
|
||||
- Production-ready
|
||||
|
||||
### v1.x (Phase 8+)
|
||||
|
||||
- Regular feature updates
|
||||
- Community-driven priorities
|
||||
- Plugin ecosystem
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### MVP Success
|
||||
|
||||
- 10 internal users daily
|
||||
- Can complete full coding session
|
||||
- <5 critical bugs
|
||||
|
||||
### Beta Success
|
||||
|
||||
- 100+ external users
|
||||
- NPS >50
|
||||
- <10 bugs per week
|
||||
|
||||
### v1.0 Success
|
||||
|
||||
- 1000+ users
|
||||
- <1% crash rate
|
||||
- Feature requests > bug reports
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Technical Risks
|
||||
|
||||
- **Process management complexity**
|
||||
- Mitigation: Extensive testing, graceful degradation
|
||||
- **SSE connection stability**
|
||||
- Mitigation: Robust reconnection logic, offline mode
|
||||
- **Performance with large sessions**
|
||||
- Mitigation: NOT a concern for MVP - defer to Phase 8
|
||||
- Accept slower performance initially, optimize later based on user feedback
|
||||
|
||||
### Product Risks
|
||||
|
||||
- **Feature creep**
|
||||
- Mitigation: Strict MVP scope, user feedback prioritization
|
||||
- **Over-optimization too early**
|
||||
- Mitigation: Focus on functionality first, optimize in Phase 8
|
||||
- Avoid premature performance optimization
|
||||
- **Platform inconsistencies**
|
||||
- Mitigation: Test on all platforms regularly
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### External
|
||||
|
||||
- OpenCode CLI availability
|
||||
- `@opencode-ai/client` contract stability
|
||||
- Electron framework updates
|
||||
|
||||
### Internal
|
||||
|
||||
- Design assets
|
||||
- Documentation
|
||||
- Testing resources
|
||||
|
||||
---
|
||||
|
||||
## Milestone Checklist
|
||||
|
||||
### Pre-Alpha
|
||||
|
||||
- [ ] All Phase 1 tasks complete
|
||||
- [ ] Can create instance and session
|
||||
- [ ] Internal demo successful
|
||||
|
||||
### Alpha
|
||||
|
||||
- [ ] All Phase 2-3 tasks complete
|
||||
- [ ] MVP feature complete
|
||||
- [ ] 5+ internal users testing
|
||||
|
||||
### Beta
|
||||
|
||||
- [ ] All Phase 4-6 tasks complete
|
||||
- [ ] Multi-instance stable
|
||||
- [ ] 50+ external testers
|
||||
|
||||
### v1.0
|
||||
|
||||
- [ ] All Phase 7 tasks complete
|
||||
- [ ] Documentation complete
|
||||
- [ ] <5 known bugs
|
||||
- [ ] Ready for public release
|
||||
|
|
@ -370,7 +370,6 @@ async function main() {
|
|||
binaryResolver,
|
||||
eventBus,
|
||||
logger: workspaceLogger,
|
||||
getServerBaseUrl: () => serverMeta.localUrl,
|
||||
nodeExtraCaCertsPath,
|
||||
})
|
||||
const fileSystemBrowser = new FileSystemBrowser({
|
||||
|
|
@ -390,7 +389,7 @@ async function main() {
|
|||
const yoloManager = new AutoAcceptManager({
|
||||
eventBus,
|
||||
logger: yoloLogger,
|
||||
replier: createOpencodePermissionReplier({ workspaceManager, logger: yoloLogger }),
|
||||
replier: createOpencodePermissionReplier({ workspaceManager }),
|
||||
persistence: sessionMetadataPersistence,
|
||||
})
|
||||
yoloManager.start()
|
||||
|
|
@ -487,7 +486,6 @@ async function main() {
|
|||
clientConnectionManager,
|
||||
remoteProxySessionManager,
|
||||
yoloManager,
|
||||
sessionMetadataPersistence,
|
||||
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
|
||||
uiDevServerUrl: uiResolution.uiDevServerUrl,
|
||||
logger,
|
||||
|
|
@ -514,7 +512,6 @@ async function main() {
|
|||
clientConnectionManager,
|
||||
remoteProxySessionManager,
|
||||
yoloManager,
|
||||
sessionMetadataPersistence,
|
||||
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
|
||||
uiDevServerUrl: undefined,
|
||||
logger,
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ describe("AutoAcceptManager persistence", () => {
|
|||
const manager = new AutoAcceptManager({ eventBus: bus, logger: noopLogger, replier: makeRecordingReplier(), persistence })
|
||||
const toggle = manager.toggle("inst", "root")
|
||||
await flushMicrotasks()
|
||||
assert.deepEqual(writes, [["inst", "root", true, undefined]])
|
||||
assert.deepEqual(writes, [["inst", "root", true]])
|
||||
assert.equal(manager.isEnabled("inst", "root"), false)
|
||||
assert.equal(changes.length, 0)
|
||||
release()
|
||||
|
|
@ -331,8 +331,8 @@ describe("AutoAcceptManager persistence", () => {
|
|||
const persistence: AutoAcceptPersistence = {
|
||||
async loadSessions() {
|
||||
return [
|
||||
{ id: "parent", parentId: null, workspaceId: "workspace", yoloEnabled: false },
|
||||
{ id: "child", parentId: null, workspaceId: "workspace", yoloEnabled: true },
|
||||
{ id: "parent", parentId: null, yoloEnabled: false },
|
||||
{ id: "child", parentId: null, yoloEnabled: true },
|
||||
]
|
||||
},
|
||||
async persist(...args) { writes.push(args) },
|
||||
|
|
@ -344,8 +344,8 @@ describe("AutoAcceptManager persistence", () => {
|
|||
await flushMicrotasks()
|
||||
assert.equal(manager.isEnabled("inst", "parent"), true)
|
||||
assert.deepEqual(writes, [
|
||||
["inst", "parent", true, "workspace"],
|
||||
["inst", "child", false, "workspace"],
|
||||
["inst", "parent", true],
|
||||
["inst", "child", false],
|
||||
])
|
||||
manager.stop()
|
||||
})
|
||||
|
|
@ -360,12 +360,12 @@ describe("AutoAcceptManager persistence", () => {
|
|||
const persistence: AutoAcceptPersistence = {
|
||||
async loadSessions() {
|
||||
return [
|
||||
{ id: "grandparent", parentId: null, workspaceId: "workspace", yoloEnabled: false },
|
||||
{ id: "parent", parentId: null, workspaceId: "workspace", yoloEnabled: false },
|
||||
{ id: "child", parentId: null, workspaceId: "workspace", yoloEnabled: true },
|
||||
{ id: "grandparent", parentId: null, yoloEnabled: false },
|
||||
{ id: "parent", parentId: null, yoloEnabled: false },
|
||||
{ id: "child", parentId: null, yoloEnabled: true },
|
||||
]
|
||||
},
|
||||
async loadSession() { return { id: "child", parentId: null, workspaceId: "workspace", yoloEnabled: true } },
|
||||
async loadSession() { return { id: "child", parentId: null, yoloEnabled: true } },
|
||||
async persist(...args) {
|
||||
writes.push(args)
|
||||
if (writes.length === 1) await firstGate
|
||||
|
|
@ -466,8 +466,6 @@ describe("AutoAcceptManager permission interception", () => {
|
|||
assert.equal(call.instanceId, "inst")
|
||||
assert.equal(call.permissionId, "perm-1")
|
||||
assert.equal(call.sessionId, "child")
|
||||
assert.equal(call.source, "v2")
|
||||
assert.equal(call.reply, "once")
|
||||
assert.equal(accepted.length, 1)
|
||||
assert.equal((accepted[0] as any).permissionId, "perm-1")
|
||||
|
||||
|
|
@ -491,7 +489,6 @@ describe("AutoAcceptManager permission interception", () => {
|
|||
await flushMicrotasks()
|
||||
|
||||
assert.equal(replier.calls.length, 1)
|
||||
assert.equal(replier.calls[0].source, "v2")
|
||||
assert.equal(replier.calls[0].permissionId, "perm-2")
|
||||
|
||||
manager.stop()
|
||||
|
|
@ -847,22 +844,22 @@ describe("AutoAcceptManager clearInstance clears pending", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("AutoAcceptManager permission.updated source inference", () => {
|
||||
it("preserves the original v2 source when permission.updated arrives", async () => {
|
||||
describe("AutoAcceptManager pending permission updates", () => {
|
||||
it("does not duplicate a pending permission reply when permission.updated arrives", async () => {
|
||||
const bus = new EventBus(noopLogger)
|
||||
const replier = makeRecordingReplier()
|
||||
const manager = new AutoAcceptManager({ eventBus: bus, logger: noopLogger, replier })
|
||||
manager.start()
|
||||
|
||||
publishSession(bus, "inst", "session.updated", { id: "solo", parentID: null })
|
||||
// yolo is OFF — permission goes to pending with source "v2"
|
||||
// Yolo is off, so the permission remains pending.
|
||||
publishInstanceEvent(bus, "inst", {
|
||||
type: "permission.v2.asked",
|
||||
properties: { id: "perm-v2", sessionID: "solo" },
|
||||
})
|
||||
await flushMicrotasks()
|
||||
|
||||
// enable yolo, then send permission.updated — should keep source "v2"
|
||||
// Enabling Yolo drains it before the follow-up update arrives.
|
||||
manager.toggle("inst", "solo")
|
||||
publishInstanceEvent(bus, "inst", {
|
||||
type: "permission.updated",
|
||||
|
|
@ -871,7 +868,6 @@ describe("AutoAcceptManager permission.updated source inference", () => {
|
|||
await flushMicrotasks()
|
||||
|
||||
assert.equal(replier.calls.length, 1)
|
||||
assert.equal(replier.calls[0].source, "v2")
|
||||
|
||||
manager.stop()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,15 +16,10 @@ import { AutoAcceptStore, type AutoAcceptSessionInfo } from "./auto-accept-store
|
|||
* so the UI stays a pure view
|
||||
*/
|
||||
|
||||
export type PermissionSource = "v2"
|
||||
export type PermissionReplyValue = "once"
|
||||
|
||||
export interface AutoAcceptReply {
|
||||
instanceId: string
|
||||
permissionId: string
|
||||
sessionId: string
|
||||
source: PermissionSource
|
||||
reply: PermissionReplyValue
|
||||
}
|
||||
|
||||
export type PermissionReplier = (reply: AutoAcceptReply) => Promise<void>
|
||||
|
|
@ -32,7 +27,6 @@ export type PermissionReplier = (reply: AutoAcceptReply) => Promise<void>
|
|||
interface PendingPermission {
|
||||
permissionId: string
|
||||
sessionId: string
|
||||
source: PermissionSource
|
||||
}
|
||||
|
||||
interface AutoAcceptManagerDeps {
|
||||
|
|
@ -44,13 +38,12 @@ interface AutoAcceptManagerDeps {
|
|||
|
||||
export interface PersistedAutoAcceptSession extends AutoAcceptSessionInfo {
|
||||
yoloEnabled: boolean
|
||||
workspaceId?: string
|
||||
}
|
||||
|
||||
export interface AutoAcceptPersistence {
|
||||
loadSessions(instanceId: string): Promise<PersistedAutoAcceptSession[]>
|
||||
loadSession?(instanceId: string, sessionId: string): Promise<PersistedAutoAcceptSession | null>
|
||||
persist(instanceId: string, rootSessionId: string, enabled: boolean, workspaceId?: string): Promise<void>
|
||||
persist(instanceId: string, rootSessionId: string, enabled: boolean): Promise<void>
|
||||
}
|
||||
|
||||
const PERMISSION_ASK_TYPES = new Set(["permission.asked"])
|
||||
|
|
@ -71,7 +64,6 @@ export class AutoAcceptManager {
|
|||
private readonly hydration = new Map<string, Promise<void>>()
|
||||
private readonly queuedEvents = new Map<string, InstanceStreamPayload[]>()
|
||||
private readonly instanceGeneration = new Map<string, number>()
|
||||
private readonly sessionWorkspaces = new Map<string, Map<string, string>>()
|
||||
private readonly mutations = new Map<string, Promise<boolean>>()
|
||||
private unsubscribe?: () => void
|
||||
|
||||
|
|
@ -135,12 +127,9 @@ export class AutoAcceptManager {
|
|||
const pending = this.deps.persistence.loadSessions(instanceId).then((sessions) => {
|
||||
if ((this.instanceGeneration.get(instanceId) ?? 0) !== generation) return
|
||||
this.store.clearInstance(instanceId)
|
||||
const workspaces = new Map<string, string>()
|
||||
for (const session of sessions) {
|
||||
this.store.upsertSession(instanceId, session)
|
||||
if (session.workspaceId) workspaces.set(session.id, session.workspaceId)
|
||||
}
|
||||
this.sessionWorkspaces.set(instanceId, workspaces)
|
||||
for (const session of sessions) {
|
||||
if (!session.yoloEnabled || this.store.familyRoot(instanceId, session.id) !== session.id) continue
|
||||
this.store.setEnabled(instanceId, session.id, true)
|
||||
|
|
@ -184,11 +173,6 @@ export class AutoAcceptManager {
|
|||
return this.store.isEnabled(instanceId, sessionId)
|
||||
}
|
||||
this.store.upsertSession(instanceId, session)
|
||||
if (session.workspaceId) {
|
||||
const workspaces = this.sessionWorkspaces.get(instanceId) ?? new Map<string, string>()
|
||||
workspaces.set(session.id, session.workspaceId)
|
||||
this.sessionWorkspaces.set(instanceId, workspaces)
|
||||
}
|
||||
const rootSessionId = this.store.familyRoot(instanceId, sessionId)
|
||||
const traversedRootSessionIds = new Set([rootSessionId])
|
||||
const enabled = !this.store.isEnabled(instanceId, rootSessionId)
|
||||
|
|
@ -196,7 +180,6 @@ export class AutoAcceptManager {
|
|||
instanceId,
|
||||
rootSessionId,
|
||||
enabled,
|
||||
this.sessionWorkspaces.get(instanceId)?.get(rootSessionId),
|
||||
)
|
||||
if ((this.instanceGeneration.get(instanceId) ?? 0) !== generation) {
|
||||
return this.store.isEnabled(instanceId, rootSessionId)
|
||||
|
|
@ -209,14 +192,12 @@ export class AutoAcceptManager {
|
|||
instanceId,
|
||||
currentRootSessionId,
|
||||
enabled,
|
||||
this.sessionWorkspaces.get(instanceId)?.get(currentRootSessionId),
|
||||
)
|
||||
if (enabled) {
|
||||
await this.deps.persistence!.persist(
|
||||
instanceId,
|
||||
persistedRootSessionId,
|
||||
false,
|
||||
this.sessionWorkspaces.get(instanceId)?.get(persistedRootSessionId),
|
||||
)
|
||||
}
|
||||
persistedRootSessionId = currentRootSessionId
|
||||
|
|
@ -248,7 +229,6 @@ export class AutoAcceptManager {
|
|||
this.hydratedInstances.delete(instanceId)
|
||||
this.hydration.delete(instanceId)
|
||||
this.queuedEvents.delete(instanceId)
|
||||
this.sessionWorkspaces.delete(instanceId)
|
||||
this.mutations.delete(instanceId)
|
||||
this.store.clearInstance(instanceId)
|
||||
this.pending.delete(instanceId)
|
||||
|
|
@ -290,11 +270,6 @@ export class AutoAcceptManager {
|
|||
const parentId = session.parentID ?? session.parentId ?? null
|
||||
const enabledBefore = this.store.enabledRoots(instanceId)
|
||||
this.store.upsertSession(instanceId, { id: sessionId, parentId, fork: session.fork })
|
||||
if (typeof session.workspaceID === "string" && session.workspaceID) {
|
||||
const workspaces = this.sessionWorkspaces.get(instanceId) ?? new Map<string, string>()
|
||||
workspaces.set(sessionId, session.workspaceID)
|
||||
this.sessionWorkspaces.set(instanceId, workspaces)
|
||||
}
|
||||
this.persistRootMigration(instanceId, enabledBefore, this.store.enabledRoots(instanceId))
|
||||
// Session ancestry may have changed as parents are discovered.
|
||||
// Re-drain pending permissions whose family root may have migrated into
|
||||
|
|
@ -330,14 +305,14 @@ export class AutoAcceptManager {
|
|||
for (const rootSessionId of added) {
|
||||
if (!enabledRoots.has(rootSessionId)) continue
|
||||
await this.deps.persistence!.persist(
|
||||
instanceId, rootSessionId, true, this.sessionWorkspaces.get(instanceId)?.get(rootSessionId),
|
||||
instanceId, rootSessionId, true,
|
||||
)
|
||||
}
|
||||
for (const rootSessionId of removed) {
|
||||
if (enabledRoots.has(rootSessionId)) continue
|
||||
if ((this.instanceGeneration.get(instanceId) ?? 0) !== generation) return false
|
||||
await this.deps.persistence!.persist(
|
||||
instanceId, rootSessionId, false, this.sessionWorkspaces.get(instanceId)?.get(rootSessionId),
|
||||
instanceId, rootSessionId, false,
|
||||
)
|
||||
}
|
||||
return false
|
||||
|
|
@ -358,15 +333,14 @@ export class AutoAcceptManager {
|
|||
const sessionId = readString(request.sessionID) ?? readString(request.sessionId)
|
||||
if (!permissionId || !sessionId) return
|
||||
|
||||
const source: PermissionSource = "v2"
|
||||
this.addPending(instanceId, { permissionId, sessionId, source })
|
||||
this.addPending(instanceId, { permissionId, sessionId })
|
||||
|
||||
if (!this.store.hasSession(instanceId, sessionId)) {
|
||||
void this.hydrateSession(instanceId, sessionId)
|
||||
return
|
||||
}
|
||||
if (!this.store.isEnabled(instanceId, sessionId)) return
|
||||
this.tryAutoAccept(instanceId, permissionId, sessionId, source)
|
||||
this.tryAutoAccept(instanceId, permissionId, sessionId)
|
||||
}
|
||||
|
||||
private async hydrateSession(instanceId: string, sessionId: string): Promise<void> {
|
||||
|
|
@ -382,10 +356,6 @@ export class AutoAcceptManager {
|
|||
|
||||
private ingestPersistedSession(instanceId: string, session: PersistedAutoAcceptSession): void {
|
||||
this.store.upsertSession(instanceId, session)
|
||||
if (!session.workspaceId) return
|
||||
const workspaces = this.sessionWorkspaces.get(instanceId) ?? new Map<string, string>()
|
||||
workspaces.set(session.id, session.workspaceId)
|
||||
this.sessionWorkspaces.set(instanceId, workspaces)
|
||||
}
|
||||
|
||||
private handlePermissionReplied(instanceId: string, properties: unknown): void {
|
||||
|
|
@ -403,7 +373,6 @@ export class AutoAcceptManager {
|
|||
instanceId: string,
|
||||
permissionId: string,
|
||||
sessionId: string,
|
||||
source: PermissionSource,
|
||||
): void {
|
||||
const key = permissionId
|
||||
if (this.inFlight.has(key)) return
|
||||
|
|
@ -412,7 +381,7 @@ export class AutoAcceptManager {
|
|||
this.inFlight.add(key)
|
||||
this.replyAttempts.set(key, attempts + 1)
|
||||
|
||||
const reply: AutoAcceptReply = { instanceId, permissionId, sessionId, source, reply: "once" }
|
||||
const reply: AutoAcceptReply = { instanceId, permissionId, sessionId }
|
||||
|
||||
void this.deps.replier(reply)
|
||||
.then(() => {
|
||||
|
|
@ -438,7 +407,7 @@ export class AutoAcceptManager {
|
|||
const root = this.store.familyRoot(instanceId, sessionId)
|
||||
for (const entry of Array.from(instancePending.values())) {
|
||||
if (this.store.hasSession(instanceId, entry.sessionId) && this.store.familyRoot(instanceId, entry.sessionId) === root) {
|
||||
this.tryAutoAccept(instanceId, entry.permissionId, entry.sessionId, entry.source)
|
||||
this.tryAutoAccept(instanceId, entry.permissionId, entry.sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -488,7 +457,6 @@ interface SessionProperties {
|
|||
parentID?: string | null
|
||||
parentId?: string | null
|
||||
fork?: unknown
|
||||
workspaceID?: string
|
||||
}
|
||||
|
||||
interface PermissionProperties {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import assert from "node:assert/strict"
|
|||
import { describe, it } from "node:test"
|
||||
import type { OpenCodeClient } from "@opencode-ai/client"
|
||||
|
||||
import type { Logger } from "../logger"
|
||||
import type { WorkspaceManager } from "../workspaces/manager"
|
||||
import { createOpencodePermissionReplier } from "./opencode-replier"
|
||||
|
||||
|
|
@ -20,14 +19,12 @@ describe("createOpencodePermissionReplier", () => {
|
|||
getSharedServiceClient: async () => client,
|
||||
ownsDirectory: async (_instanceId: string, directory: string) => directory === "/repo",
|
||||
} as unknown as WorkspaceManager
|
||||
const replier = createOpencodePermissionReplier({ workspaceManager, logger: {} as Logger })
|
||||
const replier = createOpencodePermissionReplier({ workspaceManager })
|
||||
|
||||
await replier({
|
||||
instanceId: "instance",
|
||||
sessionId: "session",
|
||||
permissionId: "permission",
|
||||
source: "v2",
|
||||
reply: "once",
|
||||
})
|
||||
|
||||
assert.deepEqual(calls, [{ sessionID: "session", requestID: "permission", reply: "once" }])
|
||||
|
|
@ -44,14 +41,12 @@ describe("createOpencodePermissionReplier", () => {
|
|||
getSharedServiceClient: async () => client,
|
||||
ownsDirectory: async () => false,
|
||||
} as unknown as WorkspaceManager
|
||||
const replier = createOpencodePermissionReplier({ workspaceManager, logger: {} as Logger })
|
||||
const replier = createOpencodePermissionReplier({ workspaceManager })
|
||||
|
||||
await assert.rejects(replier({
|
||||
instanceId: "instance",
|
||||
sessionId: "foreign-session",
|
||||
permissionId: "permission",
|
||||
source: "v2",
|
||||
reply: "once",
|
||||
}), /does not belong/)
|
||||
assert.deepEqual(calls, [])
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import type { WorkspaceManager } from "../workspaces/manager"
|
||||
import type { Logger } from "../logger"
|
||||
import { createInstanceClient } from "../workspaces/instance-client"
|
||||
import type { AutoAcceptReply, PermissionReplier } from "./auto-accept-manager"
|
||||
|
||||
interface OpencodeReplierDeps {
|
||||
workspaceManager: WorkspaceManager
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -27,7 +25,7 @@ export function createOpencodePermissionReplier(deps: OpencodeReplierDeps): Perm
|
|||
await client.permission.reply({
|
||||
sessionID: reply.sessionId,
|
||||
requestID: reply.permissionId,
|
||||
reply: reply.reply,
|
||||
reply: "once",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,14 +82,12 @@ describe("OpenCode Yolo persistence", () => {
|
|||
id: "root",
|
||||
parentId: null,
|
||||
fork: undefined,
|
||||
workspaceId: "workspace",
|
||||
yoloEnabled: true,
|
||||
},
|
||||
{
|
||||
id: "second-page",
|
||||
parentId: null,
|
||||
fork: undefined,
|
||||
workspaceId: "workspace",
|
||||
yoloEnabled: false,
|
||||
},
|
||||
])
|
||||
|
|
@ -120,7 +118,6 @@ describe("OpenCode Yolo persistence", () => {
|
|||
id: "worktree",
|
||||
parentId: null,
|
||||
fork: { sessionID: "root", boundary: { type: "through", messageID: "message" } },
|
||||
workspaceId: "workspace",
|
||||
yoloEnabled: true,
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ export function createOpencodeYoloPersistence(
|
|||
id: session.id,
|
||||
parentId: session.parentID ?? null,
|
||||
fork: session.fork,
|
||||
workspaceId: session.location.workspaceID,
|
||||
yoloEnabled: sessionState(settings, session.id).yoloEnabled === true,
|
||||
})
|
||||
const updateYolo = (
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import { registerUsageRoutes } from "./routes/usage"
|
|||
import { ServerMeta } from "../api-types"
|
||||
import { InstanceStore } from "../storage/instance-store"
|
||||
import type { AutoAcceptManager } from "../permissions/auto-accept-manager"
|
||||
import type { OpencodeYoloPersistence } from "../permissions/opencode-yolo-metadata"
|
||||
import type { AuthManager } from "../auth/manager"
|
||||
import { registerAuthRoutes } from "./routes/auth"
|
||||
import { sendUnauthorized, wantsHtml } from "../auth/http-auth"
|
||||
|
|
@ -64,7 +63,6 @@ interface HttpServerDeps {
|
|||
clientConnectionManager: ClientConnectionManager
|
||||
remoteProxySessionManager: RemoteProxySessionManager
|
||||
yoloManager: AutoAcceptManager
|
||||
sessionMetadataPersistence: OpencodeYoloPersistence
|
||||
uiStaticDir: string
|
||||
uiDevServerUrl?: string
|
||||
logger: Logger
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ function createManager(rootDir: string) {
|
|||
binaryResolver: { resolveDefault: () => ({ path: process.execPath, label: "Node.js", version: process.version }) },
|
||||
eventBus: new EventBus(logger),
|
||||
logger,
|
||||
getServerBaseUrl: () => "http://127.0.0.1:3000",
|
||||
sharedService,
|
||||
} as unknown as ConstructorParameters<typeof WorkspaceManager>[0])
|
||||
return manager
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ function createHarness(service = new ControlledSharedService(), overrides: Recor
|
|||
binaryResolver: { resolveDefault: () => ({ path: process.execPath, label: "OpenCode V2" }) } as never,
|
||||
eventBus,
|
||||
logger: pino({ level: "silent" }),
|
||||
getServerBaseUrl: () => "http://127.0.0.1:4000",
|
||||
sharedService: service,
|
||||
...overrides,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ interface WorkspaceManagerOptions {
|
|||
binaryResolver: BinaryResolver
|
||||
eventBus: EventBus
|
||||
logger: Logger
|
||||
getServerBaseUrl: () => string
|
||||
/** Optional CA bundle path to trust CodeNomad HTTPS certs. */
|
||||
nodeExtraCaCertsPath?: string
|
||||
sharedService?: SharedService
|
||||
|
|
@ -143,7 +142,6 @@ export class WorkspaceManager {
|
|||
private readonly cancelledCreationRequests = new Set<string>()
|
||||
private shuttingDown = false
|
||||
private readonly sharedService: SharedService
|
||||
private serviceEndpoint?: Endpoint
|
||||
private serviceAuthorization?: string
|
||||
|
||||
constructor(private readonly options: WorkspaceManagerOptions) {
|
||||
|
|
@ -172,7 +170,6 @@ export class WorkspaceManager {
|
|||
if (!this.workspaces.get(id)?.[WORKSPACE_STATE].published) return undefined
|
||||
try {
|
||||
const [endpoint, headers] = await Promise.all([this.sharedService.endpoint(), this.sharedService.headers()])
|
||||
this.serviceEndpoint = endpoint
|
||||
this.serviceAuthorization = headers?.authorization
|
||||
return endpoint
|
||||
} catch (error) {
|
||||
|
|
@ -505,8 +502,7 @@ export class WorkspaceManager {
|
|||
? this.requireWslServiceDirectory(workspacePath, launch.wslDistro, timeoutMs)
|
||||
: workspacePath
|
||||
record.location = { directory: serviceDirectory }
|
||||
const [endpoint, headers, location] = await Promise.all([
|
||||
this.sharedService.endpoint(ensureOptions),
|
||||
const [headers, location] = await Promise.all([
|
||||
this.sharedService.headers(ensureOptions),
|
||||
this.sharedService.validateLocation(
|
||||
{ directory: serviceDirectory },
|
||||
|
|
@ -514,7 +510,6 @@ export class WorkspaceManager {
|
|||
ensureOptions,
|
||||
),
|
||||
])
|
||||
this.serviceEndpoint = endpoint
|
||||
this.serviceAuthorization = headers?.authorization
|
||||
record.location = { directory: location.directory, workspaceID: location.workspaceID }
|
||||
this.throwIfCancelled(record)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { Service, type Endpoint, type EnsureOptions, type Info, type StopOptions
|
|||
import { spawn, type ChildProcess } from "node:child_process"
|
||||
import { createHash, randomUUID } from "node:crypto"
|
||||
import type { Stats } from "node:fs"
|
||||
import { appendFile, lstat, mkdir, open, readdir, rename, rm } from "node:fs/promises"
|
||||
import { appendFile, lstat, mkdir, open, readFile, readdir, rename, rm } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import {
|
||||
getProcessStartIdentity,
|
||||
|
|
@ -533,9 +533,7 @@ export class OpenCodeSharedService {
|
|||
if (!file) return undefined
|
||||
const [info, contenders] = await Promise.all([
|
||||
readSecureServiceInfo(file),
|
||||
contenderFile ? open(contenderFile, "r").then(async (handle) => {
|
||||
try { return await handle.readFile("utf8") } finally { await handle.close() }
|
||||
}).catch(() => "") : "",
|
||||
contenderFile ? readFile(contenderFile, "utf8").catch(() => "") : "",
|
||||
])
|
||||
if (!info || !started) return undefined
|
||||
if (info.url !== endpoint.url || info.password !== endpoint.auth?.password) return undefined
|
||||
|
|
@ -644,9 +642,7 @@ export class OpenCodeSharedService {
|
|||
if (!launch || !info || info.url !== endpoint.url || info.password !== endpoint.auth?.password) return false
|
||||
if (launch.servicePid !== undefined) return launch.servicePid === info.pid
|
||||
if (launch.contenderFile) {
|
||||
const contenders = await open(launch.contenderFile, "r").then(async (handle) => {
|
||||
try { return await handle.readFile("utf8") } finally { await handle.close() }
|
||||
}).catch(() => "")
|
||||
const contenders = await readFile(launch.contenderFile, "utf8").catch(() => "")
|
||||
if (contenders.split(/\r?\n/).includes(String(info.pid))) return true
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
import { isInitialMessageLoad } from "./message-loading-visibility.ts"
|
||||
|
||||
describe("message loading visibility", () => {
|
||||
it("shows loading UI only before the first message snapshot", () => {
|
||||
assert.equal(isInitialMessageLoad(true, 0), true)
|
||||
assert.equal(isInitialMessageLoad(false, 0), false)
|
||||
assert.equal(isInitialMessageLoad(true, 1), false)
|
||||
assert.equal(isInitialMessageLoad(true, 20), false)
|
||||
})
|
||||
|
||||
it("keeps established timeline state during a background refresh", () => {
|
||||
assert.equal(isInitialMessageLoad(true, 12), false)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export function isInitialMessageLoad(loading: boolean, messageCount: number) {
|
||||
return loading && messageCount === 0
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import BrandedEmptyState from "./branded-empty-state"
|
|||
import LoadErrorState from "./load-error-state"
|
||||
import MessageBlock from "./message-block"
|
||||
import { getMessageAnchorId } from "./message-anchors"
|
||||
import { isInitialMessageLoad } from "./message-loading-visibility"
|
||||
import MessageTimeline, { buildTimelineSegments, type TimelineSegment } from "./message-timeline"
|
||||
import { getTimelineRecordSignature } from "./message-timeline-projection"
|
||||
import VirtualFollowList, { type VirtualExplicitBottomPinIntent, type VirtualFollowListApi, type VirtualFollowListState, type VirtualFollowScrollSnapshot } from "./virtual-follow-list"
|
||||
|
|
@ -890,7 +889,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||
</Show>
|
||||
</Show>
|
||||
|
||||
<Show when={isInitialMessageLoad(Boolean(props.loading), messageIds().length)}>
|
||||
<Show when={Boolean(props.loading) && messageIds().length === 0}>
|
||||
<div class="loading-state">
|
||||
<div class="spinner" />
|
||||
<p>{t("messageSection.loading.messages")}</p>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,14 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { it } from "node:test"
|
||||
import type { Pty } from "@opencode-ai/client"
|
||||
import { createEffect, createRoot } from "solid-js"
|
||||
import { createPtyStore, type PtyApi } from "./pty-store.ts"
|
||||
|
||||
const unusedPty = (): Pty => ({
|
||||
id: "unused",
|
||||
title: "unused",
|
||||
command: "npm",
|
||||
args: [],
|
||||
cwd: "/repo",
|
||||
status: "running",
|
||||
pid: 42,
|
||||
})
|
||||
|
||||
it("does not subscribe a calling effect to internal PTY loading state", async () => {
|
||||
let listCalls = 0
|
||||
let effectRuns = 0
|
||||
const api: PtyApi = {
|
||||
list: async () => { listCalls += 1; return [] },
|
||||
get: async () => unusedPty(),
|
||||
updateTitle: async () => unusedPty(),
|
||||
updateTitle: async () => { throw new Error("unused") },
|
||||
remove: async () => {},
|
||||
}
|
||||
const store = createPtyStore(() => api)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ describe("native PTY adapter", () => {
|
|||
const client = {
|
||||
pty: {
|
||||
list: async (input: unknown) => { calls.push(["list", input]); return { data: [pty("one")] } },
|
||||
get: async (input: unknown) => { calls.push(["get", input]); return { data: pty("one") } },
|
||||
update: async (input: unknown) => { calls.push(["update", input]); return { data: { ...pty("one"), title: "renamed" } } },
|
||||
remove: async (input: unknown) => { calls.push(["remove", input]) },
|
||||
},
|
||||
|
|
@ -27,13 +26,11 @@ describe("native PTY adapter", () => {
|
|||
const api = createPtyApi(client)
|
||||
|
||||
assert.deepEqual(await api.list("/repo/worktree"), [pty("one")])
|
||||
assert.equal((await api.get("/repo/worktree", "one")).id, "one")
|
||||
assert.equal((await api.updateTitle("/repo/worktree", "one", "renamed")).title, "renamed")
|
||||
await api.remove("/repo/worktree", "one")
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
["list", { location: { directory: "/repo/worktree" } }],
|
||||
["get", { ptyID: "one", location: { directory: "/repo/worktree" } }],
|
||||
["update", { ptyID: "one", location: { directory: "/repo/worktree" }, title: "renamed" }],
|
||||
["remove", { ptyID: "one", location: { directory: "/repo/worktree" } }],
|
||||
])
|
||||
|
|
@ -45,7 +42,6 @@ describe("PTY store", () => {
|
|||
const lists: string[] = []
|
||||
const api: PtyApi = {
|
||||
list: async (directory) => { lists.push(directory); return [pty(directory, directory)] },
|
||||
get: async () => pty("unused"),
|
||||
updateTitle: async () => pty("unused"),
|
||||
remove: async () => {},
|
||||
}
|
||||
|
|
@ -71,7 +67,6 @@ describe("PTY store", () => {
|
|||
let items = [pty("one")]
|
||||
const api: PtyApi = {
|
||||
list: async () => items,
|
||||
get: async () => items[0]!,
|
||||
updateTitle: async (_directory, id, title) => {
|
||||
controls.push(["update", id, title])
|
||||
items = [{ ...items[0]!, title }]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type { OpenCodeClient, Pty } from "@opencode-ai/client"
|
|||
|
||||
export interface PtyApi {
|
||||
list(directory: string): Promise<Pty[]>
|
||||
get(directory: string, ptyId: string): Promise<Pty>
|
||||
updateTitle(directory: string, ptyId: string, title: string): Promise<Pty>
|
||||
remove(directory: string, ptyId: string): Promise<void>
|
||||
}
|
||||
|
|
@ -27,7 +26,6 @@ export function createPtyApi(client: OpenCodeClient): PtyApi {
|
|||
const location = (directory: string) => ({ directory })
|
||||
return {
|
||||
list: async (directory) => (await client.pty.list({ location: location(directory) })).data,
|
||||
get: async (directory, ptyId) => (await client.pty.get({ ptyID: ptyId, location: location(directory) })).data,
|
||||
updateTitle: async (directory, ptyId, title) => (
|
||||
await client.pty.update({ ptyID: ptyId, location: location(directory), title })
|
||||
).data,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue