From 775c6c6ca11327fed13c41d563441868eeaf7df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 18 Aug 2026 23:21:43 +0200 Subject: [PATCH] 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. --- .github/workflows/pr-build.yml | 1 - MIGRATION_V2.md | 8 - dev-docs/INDEX.md | 89 +--- dev-docs/SUMMARY.md | 173 +------- dev-docs/build-roadmap.md | 391 ------------------ packages/server/src/index.ts | 5 +- .../permissions/auto-accept-manager.test.ts | 30 +- .../src/permissions/auto-accept-manager.ts | 46 +-- .../src/permissions/opencode-replier.test.ts | 9 +- .../src/permissions/opencode-replier.ts | 4 +- .../opencode-yolo-metadata.test.ts | 3 - .../src/permissions/opencode-yolo-metadata.ts | 1 - packages/server/src/server/http-server.ts | 2 - .../__tests__/workspace-identity.test.ts | 1 - .../server/src/workspaces/manager.test.ts | 1 - packages/server/src/workspaces/manager.ts | 7 +- .../server/src/workspaces/opencode-service.ts | 10 +- .../message-loading-visibility.test.ts | 16 - .../components/message-loading-visibility.ts | 3 - .../ui/src/components/message-section.tsx | 3 +- .../src/stores/pty-store-reactivity.test.ts | 14 +- packages/ui/src/stores/pty-store.test.ts | 5 - packages/ui/src/stores/pty-store.ts | 2 - 23 files changed, 38 insertions(+), 786 deletions(-) delete mode 100644 dev-docs/build-roadmap.md delete mode 100644 packages/ui/src/components/message-loading-visibility.test.ts delete mode 100644 packages/ui/src/components/message-loading-visibility.ts diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index d89fd3f9..9f9590da 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -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 diff --git a/MIGRATION_V2.md b/MIGRATION_V2.md index 9900043c..82675c63 100644 --- a/MIGRATION_V2.md +++ b/MIGRATION_V2.md @@ -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. diff --git a/dev-docs/INDEX.md b/dev-docs/INDEX.md index ed48cae2..e68aef32 100644 --- a/dev-docs/INDEX.md +++ b/dev-docs/INDEX.md @@ -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._ diff --git a/dev-docs/SUMMARY.md b/dev-docs/SUMMARY.md index c4949127..22da0017 100644 --- a/dev-docs/SUMMARY.md +++ b/dev-docs/SUMMARY.md @@ -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! 🚀 diff --git a/dev-docs/build-roadmap.md b/dev-docs/build-roadmap.md deleted file mode 100644 index 31aa48cf..00000000 --- a/dev-docs/build-roadmap.md +++ /dev/null @@ -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 diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b7db7c55..7ae85fae 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -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, diff --git a/packages/server/src/permissions/auto-accept-manager.test.ts b/packages/server/src/permissions/auto-accept-manager.test.ts index e7eb7fe7..117a5ffe 100644 --- a/packages/server/src/permissions/auto-accept-manager.test.ts +++ b/packages/server/src/permissions/auto-accept-manager.test.ts @@ -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() }) diff --git a/packages/server/src/permissions/auto-accept-manager.ts b/packages/server/src/permissions/auto-accept-manager.ts index 86db4f5d..41a81eb0 100644 --- a/packages/server/src/permissions/auto-accept-manager.ts +++ b/packages/server/src/permissions/auto-accept-manager.ts @@ -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 @@ -32,7 +27,6 @@ export type PermissionReplier = (reply: AutoAcceptReply) => Promise 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 loadSession?(instanceId: string, sessionId: string): Promise - persist(instanceId: string, rootSessionId: string, enabled: boolean, workspaceId?: string): Promise + persist(instanceId: string, rootSessionId: string, enabled: boolean): Promise } const PERMISSION_ASK_TYPES = new Set(["permission.asked"]) @@ -71,7 +64,6 @@ export class AutoAcceptManager { private readonly hydration = new Map>() private readonly queuedEvents = new Map() private readonly instanceGeneration = new Map() - private readonly sessionWorkspaces = new Map>() private readonly mutations = new Map>() 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() 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() - 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() - 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 { @@ -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() - 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 { diff --git a/packages/server/src/permissions/opencode-replier.test.ts b/packages/server/src/permissions/opencode-replier.test.ts index c7722a0f..73a72fb6 100644 --- a/packages/server/src/permissions/opencode-replier.test.ts +++ b/packages/server/src/permissions/opencode-replier.test.ts @@ -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, []) }) diff --git a/packages/server/src/permissions/opencode-replier.ts b/packages/server/src/permissions/opencode-replier.ts index 2e986d80..06a5f997 100644 --- a/packages/server/src/permissions/opencode-replier.ts +++ b/packages/server/src/permissions/opencode-replier.ts @@ -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", }) } } diff --git a/packages/server/src/permissions/opencode-yolo-metadata.test.ts b/packages/server/src/permissions/opencode-yolo-metadata.test.ts index 3d8854e0..b60cded1 100644 --- a/packages/server/src/permissions/opencode-yolo-metadata.test.ts +++ b/packages/server/src/permissions/opencode-yolo-metadata.test.ts @@ -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, }) }) diff --git a/packages/server/src/permissions/opencode-yolo-metadata.ts b/packages/server/src/permissions/opencode-yolo-metadata.ts index 3606bfa2..eee343f6 100644 --- a/packages/server/src/permissions/opencode-yolo-metadata.ts +++ b/packages/server/src/permissions/opencode-yolo-metadata.ts @@ -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 = ( diff --git a/packages/server/src/server/http-server.ts b/packages/server/src/server/http-server.ts index e77294ff..524a22b8 100644 --- a/packages/server/src/server/http-server.ts +++ b/packages/server/src/server/http-server.ts @@ -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 diff --git a/packages/server/src/workspaces/__tests__/workspace-identity.test.ts b/packages/server/src/workspaces/__tests__/workspace-identity.test.ts index 6d8e4b12..cc9eebaf 100644 --- a/packages/server/src/workspaces/__tests__/workspace-identity.test.ts +++ b/packages/server/src/workspaces/__tests__/workspace-identity.test.ts @@ -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[0]) return manager diff --git a/packages/server/src/workspaces/manager.test.ts b/packages/server/src/workspaces/manager.test.ts index f55e175e..8ffcf105 100644 --- a/packages/server/src/workspaces/manager.test.ts +++ b/packages/server/src/workspaces/manager.test.ts @@ -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, }) diff --git a/packages/server/src/workspaces/manager.ts b/packages/server/src/workspaces/manager.ts index df24c36f..d4e2b159 100644 --- a/packages/server/src/workspaces/manager.ts +++ b/packages/server/src/workspaces/manager.ts @@ -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() 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) diff --git a/packages/server/src/workspaces/opencode-service.ts b/packages/server/src/workspaces/opencode-service.ts index 4b35f168..13cec1f9 100644 --- a/packages/server/src/workspaces/opencode-service.ts +++ b/packages/server/src/workspaces/opencode-service.ts @@ -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 diff --git a/packages/ui/src/components/message-loading-visibility.test.ts b/packages/ui/src/components/message-loading-visibility.test.ts deleted file mode 100644 index 82a3df5d..00000000 --- a/packages/ui/src/components/message-loading-visibility.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/packages/ui/src/components/message-loading-visibility.ts b/packages/ui/src/components/message-loading-visibility.ts deleted file mode 100644 index 4b4c5c33..00000000 --- a/packages/ui/src/components/message-loading-visibility.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function isInitialMessageLoad(loading: boolean, messageCount: number) { - return loading && messageCount === 0 -} diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 8cd93bf0..d0bd070f 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -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) { - +

{t("messageSection.loading.messages")}

diff --git a/packages/ui/src/stores/pty-store-reactivity.test.ts b/packages/ui/src/stores/pty-store-reactivity.test.ts index c9e2c2ba..9a6fd063 100644 --- a/packages/ui/src/stores/pty-store-reactivity.test.ts +++ b/packages/ui/src/stores/pty-store-reactivity.test.ts @@ -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) diff --git a/packages/ui/src/stores/pty-store.test.ts b/packages/ui/src/stores/pty-store.test.ts index 1a844a8e..1e0a75f8 100644 --- a/packages/ui/src/stores/pty-store.test.ts +++ b/packages/ui/src/stores/pty-store.test.ts @@ -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 }] diff --git a/packages/ui/src/stores/pty-store.ts b/packages/ui/src/stores/pty-store.ts index 01b42c24..c76903cf 100644 --- a/packages/ui/src/stores/pty-store.ts +++ b/packages/ui/src/stores/pty-store.ts @@ -3,7 +3,6 @@ import type { OpenCodeClient, Pty } from "@opencode-ai/client" export interface PtyApi { list(directory: string): Promise - get(directory: string, ptyId: string): Promise updateTitle(directory: string, ptyId: string, title: string): Promise remove(directory: string, ptyId: string): Promise } @@ -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,