@dreb/coding-agent 2.34.3 → 2.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/dist/cli/args.d.ts +3 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +3 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +30 -2
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +172 -130
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/export-html/template.js +1 -1
  12. package/dist/core/footer-data-provider.d.ts.map +1 -1
  13. package/dist/core/footer-data-provider.js +5 -102
  14. package/dist/core/footer-data-provider.js.map +1 -1
  15. package/dist/core/git-branch.d.ts +19 -0
  16. package/dist/core/git-branch.d.ts.map +1 -0
  17. package/dist/core/git-branch.js +109 -0
  18. package/dist/core/git-branch.js.map +1 -0
  19. package/dist/core/sdk.d.ts.map +1 -1
  20. package/dist/core/sdk.js +1 -1
  21. package/dist/core/sdk.js.map +1 -1
  22. package/dist/core/session-manager.d.ts +21 -0
  23. package/dist/core/session-manager.d.ts.map +1 -1
  24. package/dist/core/session-manager.js +117 -42
  25. package/dist/core/session-manager.js.map +1 -1
  26. package/dist/core/settings-manager.d.ts +6 -0
  27. package/dist/core/settings-manager.d.ts.map +1 -1
  28. package/dist/core/settings-manager.js +8 -0
  29. package/dist/core/settings-manager.js.map +1 -1
  30. package/dist/{modes/interactive → core}/tab-title.d.ts +12 -3
  31. package/dist/core/tab-title.d.ts.map +1 -0
  32. package/dist/{modes/interactive → core}/tab-title.js +58 -16
  33. package/dist/core/tab-title.js.map +1 -0
  34. package/dist/core/tools/search.d.ts.map +1 -1
  35. package/dist/core/tools/search.js +4 -1
  36. package/dist/core/tools/search.js.map +1 -1
  37. package/dist/core/tools/subagent.d.ts +63 -2
  38. package/dist/core/tools/subagent.d.ts.map +1 -1
  39. package/dist/core/tools/subagent.js +459 -73
  40. package/dist/core/tools/subagent.js.map +1 -1
  41. package/dist/core/tools/web.d.ts +1 -1
  42. package/dist/core/tools/web.d.ts.map +1 -1
  43. package/dist/core/tools/web.js +60 -16
  44. package/dist/core/tools/web.js.map +1 -1
  45. package/dist/main.d.ts +10 -0
  46. package/dist/main.d.ts.map +1 -1
  47. package/dist/main.js +54 -4
  48. package/dist/main.js.map +1 -1
  49. package/dist/modes/index.d.ts +2 -2
  50. package/dist/modes/index.d.ts.map +1 -1
  51. package/dist/modes/index.js +1 -1
  52. package/dist/modes/index.js.map +1 -1
  53. package/dist/modes/interactive/components/session-selector.d.ts +1 -1
  54. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  55. package/dist/modes/interactive/components/session-selector.js +4 -40
  56. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  57. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  58. package/dist/modes/interactive/interactive-mode.js +2 -2
  59. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  60. package/dist/modes/rpc/index.d.ts +2 -2
  61. package/dist/modes/rpc/index.d.ts.map +1 -1
  62. package/dist/modes/rpc/index.js.map +1 -1
  63. package/dist/modes/rpc/rpc-client.d.ts +114 -6
  64. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  65. package/dist/modes/rpc/rpc-client.js +140 -2
  66. package/dist/modes/rpc/rpc-client.js.map +1 -1
  67. package/dist/modes/rpc/rpc-mode.d.ts +97 -0
  68. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  69. package/dist/modes/rpc/rpc-mode.js +656 -27
  70. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  71. package/dist/modes/rpc/rpc-types.d.ts +312 -1
  72. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  73. package/dist/modes/rpc/rpc-types.js.map +1 -1
  74. package/docs/dashboard.md +254 -0
  75. package/docs/development.md +8 -5
  76. package/docs/rpc.md +652 -5
  77. package/examples/sdk/12-full-control.ts +1 -1
  78. package/package.json +1 -1
  79. package/dist/modes/interactive/tab-title.d.ts.map +0 -1
  80. package/dist/modes/interactive/tab-title.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-types.js","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * RPC protocol types for headless operation.\n *\n * Commands are sent as JSON lines on stdin.\n * Responses and events are emitted as JSON lines on stdout.\n */\n\nimport type { AgentMessage, ThinkingLevel } from \"@dreb/agent-core\";\nimport type { ImageContent, Model } from \"@dreb/ai\";\nimport type { SessionStats } from \"../../core/agent-session.js\";\nimport type { BashResult } from \"../../core/bash-executor.js\";\nimport type { CompactionResult } from \"../../core/compaction/index.js\";\nimport type { SourceInfo } from \"../../core/source-info.js\";\n\n// ============================================================================\n// RPC Commands (stdin)\n// ============================================================================\n\nexport type RpcCommand =\n\t// Prompting\n\t| { id?: string; type: \"prompt\"; message: string; images?: ImageContent[]; streamingBehavior?: \"steer\" | \"followUp\" }\n\t| { id?: string; type: \"steer\"; message: string; images?: ImageContent[] }\n\t| { id?: string; type: \"follow_up\"; message: string; images?: ImageContent[] }\n\t| { id?: string; type: \"abort\" }\n\t| { id?: string; type: \"new_session\"; parentSession?: string }\n\n\t// State\n\t| { id?: string; type: \"get_state\" }\n\n\t// Model\n\t| { id?: string; type: \"set_model\"; provider: string; modelId: string }\n\t| { id?: string; type: \"resolve_model\"; pattern: string }\n\t| { id?: string; type: \"cycle_model\" }\n\t| { id?: string; type: \"get_available_models\" }\n\n\t// Buddy — hatch/reroll run inside the agent process so API keys never leave\n\t| { id?: string; type: \"buddy_hatch\" }\n\t| { id?: string; type: \"buddy_reroll\" }\n\n\t// Thinking\n\t| { id?: string; type: \"set_thinking_level\"; level: ThinkingLevel }\n\t| { id?: string; type: \"cycle_thinking_level\" }\n\n\t// Queue modes\n\t| { id?: string; type: \"set_steering_mode\"; mode: \"all\" | \"one-at-a-time\" }\n\t| { id?: string; type: \"set_follow_up_mode\"; mode: \"all\" | \"one-at-a-time\" }\n\n\t// Compaction\n\t| { id?: string; type: \"compact\"; customInstructions?: string }\n\t| { id?: string; type: \"set_auto_compaction\"; enabled: boolean }\n\n\t// Retry\n\t| { id?: string; type: \"set_auto_retry\"; enabled: boolean }\n\t| { id?: string; type: \"abort_retry\" }\n\n\t// Bash\n\t| { id?: string; type: \"bash\"; command: string }\n\t| { id?: string; type: \"abort_bash\" }\n\n\t// Session\n\t| { id?: string; type: \"get_session_stats\" }\n\t| { id?: string; type: \"get_performance_stats\" }\n\t| { id?: string; type: \"export_html\"; outputPath?: string }\n\t| { id?: string; type: \"switch_session\"; sessionPath: string }\n\t| { id?: string; type: \"fork\"; entryId: string }\n\t| { id?: string; type: \"get_fork_messages\" }\n\t| { id?: string; type: \"get_last_assistant_text\" }\n\t| { id?: string; type: \"set_session_name\"; name: string }\n\n\t// Messages\n\t| { id?: string; type: \"get_messages\" }\n\n\t// Commands (available for invocation via prompt)\n\t| { id?: string; type: \"get_commands\" }\n\n\t// Session listing\n\t| { id?: string; type: \"list_sessions\" }\n\n\t// Version\n\t| { id?: string; type: \"get_version\" };\n\n// ============================================================================\n// RPC Slash Command (for get_commands response)\n// ============================================================================\n\n/** A command available for invocation via prompt */\nexport interface RpcSlashCommand {\n\t/** Command name (without leading slash) */\n\tname: string;\n\t/** Human-readable description */\n\tdescription?: string;\n\t/** What kind of command this is */\n\tsource: \"extension\" | \"prompt\" | \"skill\";\n\t/** Source metadata for the owning resource */\n\tsourceInfo: SourceInfo;\n}\n\n// ============================================================================\n// RPC State\n// ============================================================================\n\nexport interface RpcSessionState {\n\tmodel?: Model<any>;\n\tthinkingLevel: ThinkingLevel;\n\tisStreaming: boolean;\n\tisCompacting: boolean;\n\tsteeringMode: \"all\" | \"one-at-a-time\";\n\tfollowUpMode: \"all\" | \"one-at-a-time\";\n\tsessionFile?: string;\n\tsessionId: string;\n\tsessionName?: string;\n\tautoCompactionEnabled: boolean;\n\tmessageCount: number;\n\tpendingMessageCount: number;\n\t/** Non-empty when the model was changed from the user's saved preference\n\t * (e.g. saved model unavailable after restart). */\n\tmodelFallbackMessage?: string;\n}\n\n// ============================================================================\n// RPC Responses (stdout)\n// ============================================================================\n\n// Success responses with data\nexport type RpcResponse =\n\t// Prompting (async - events follow)\n\t| { id?: string; type: \"response\"; command: \"prompt\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"steer\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"follow_up\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"abort\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"new_session\"; success: true; data: { cancelled: boolean } }\n\n\t// State\n\t| { id?: string; type: \"response\"; command: \"get_state\"; success: true; data: RpcSessionState }\n\n\t// Model\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"set_model\";\n\t\t\tsuccess: true;\n\t\t\tdata: Model<any>;\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"resolve_model\";\n\t\t\tsuccess: true;\n\t\t\tdata: { model: Model<any>; warning?: string } | null;\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"cycle_model\";\n\t\t\tsuccess: true;\n\t\t\tdata: { model: Model<any>; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_available_models\";\n\t\t\tsuccess: true;\n\t\t\tdata: { models: Model<any>[] };\n\t }\n\n\t// Buddy\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"buddy_hatch\";\n\t\t\tsuccess: true;\n\t\t\tdata: { state: import(\"../../core/buddy/buddy-types.js\").BuddyState };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"buddy_reroll\";\n\t\t\tsuccess: true;\n\t\t\tdata: { state: import(\"../../core/buddy/buddy-types.js\").BuddyState };\n\t }\n\n\t// Thinking\n\t| { id?: string; type: \"response\"; command: \"set_thinking_level\"; success: true }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"cycle_thinking_level\";\n\t\t\tsuccess: true;\n\t\t\tdata: { level: ThinkingLevel } | null;\n\t }\n\n\t// Queue modes\n\t| { id?: string; type: \"response\"; command: \"set_steering_mode\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"set_follow_up_mode\"; success: true }\n\n\t// Compaction\n\t| { id?: string; type: \"response\"; command: \"compact\"; success: true; data: CompactionResult }\n\t| { id?: string; type: \"response\"; command: \"set_auto_compaction\"; success: true }\n\n\t// Retry\n\t| { id?: string; type: \"response\"; command: \"set_auto_retry\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"abort_retry\"; success: true }\n\n\t// Bash\n\t| { id?: string; type: \"response\"; command: \"bash\"; success: true; data: BashResult }\n\t| { id?: string; type: \"response\"; command: \"abort_bash\"; success: true }\n\n\t// Session\n\t| { id?: string; type: \"response\"; command: \"get_session_stats\"; success: true; data: SessionStats }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_performance_stats\";\n\t\t\tsuccess: true;\n\t\t\tdata: { models: Array<{ provider: string; modelId: string; median: number; mean: number; count: number }> };\n\t }\n\t| { id?: string; type: \"response\"; command: \"export_html\"; success: true; data: { path: string } }\n\t| { id?: string; type: \"response\"; command: \"switch_session\"; success: true; data: { cancelled: boolean } }\n\t| { id?: string; type: \"response\"; command: \"fork\"; success: true; data: { text: string; cancelled: boolean } }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_fork_messages\";\n\t\t\tsuccess: true;\n\t\t\tdata: { messages: Array<{ entryId: string; text: string }> };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_last_assistant_text\";\n\t\t\tsuccess: true;\n\t\t\tdata: { text: string | null };\n\t }\n\t| { id?: string; type: \"response\"; command: \"set_session_name\"; success: true }\n\n\t// Messages\n\t| { id?: string; type: \"response\"; command: \"get_messages\"; success: true; data: { messages: AgentMessage[] } }\n\n\t// Commands\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_commands\";\n\t\t\tsuccess: true;\n\t\t\tdata: { commands: RpcSlashCommand[] };\n\t }\n\n\t// Session listing\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"list_sessions\";\n\t\t\tsuccess: true;\n\t\t\tdata: { sessions: RpcSessionInfo[] };\n\t }\n\n\t// Version\n\t| { id?: string; type: \"response\"; command: \"get_version\"; success: true; data: { version: string } }\n\n\t// Error response (any command can fail)\n\t| { id?: string; type: \"response\"; command: string; success: false; error: string };\n\n// ============================================================================\n// Session Info (for list_sessions response)\n// ============================================================================\n\n/** Session metadata returned by list_sessions */\nexport interface RpcSessionInfo {\n\t/** Full path to the session JSONL file */\n\tpath: string;\n\t/** Session UUID */\n\tid: string;\n\t/** Working directory where the session was started */\n\tcwd: string;\n\t/** User-defined display name */\n\tname?: string;\n\t/** ISO timestamp of session creation */\n\tcreated: string;\n\t/** ISO timestamp of last modification */\n\tmodified: string;\n\t/** Number of messages in the session */\n\tmessageCount: number;\n\t/** First user message text */\n\tfirstMessage: string;\n}\n\n// ============================================================================\n// Extension UI Events (stdout)\n// ============================================================================\n\n/** Emitted when an extension needs user input */\nexport type RpcExtensionUIRequest =\n\t| { type: \"extension_ui_request\"; id: string; method: \"select\"; title: string; options: string[]; timeout?: number }\n\t| { type: \"extension_ui_request\"; id: string; method: \"confirm\"; title: string; message: string; timeout?: number }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"input\";\n\t\t\ttitle: string;\n\t\t\tplaceholder?: string;\n\t\t\ttimeout?: number;\n\t }\n\t| { type: \"extension_ui_request\"; id: string; method: \"editor\"; title: string; prefill?: string }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"notify\";\n\t\t\tmessage: string;\n\t\t\tnotifyType?: \"info\" | \"warning\" | \"error\";\n\t }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"setStatus\";\n\t\t\tstatusKey: string;\n\t\t\tstatusText: string | undefined;\n\t }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"setWidget\";\n\t\t\twidgetKey: string;\n\t\t\twidgetLines: string[] | undefined;\n\t\t\twidgetPlacement?: \"aboveEditor\" | \"belowEditor\";\n\t }\n\t| { type: \"extension_ui_request\"; id: string; method: \"setTitle\"; title: string }\n\t| { type: \"extension_ui_request\"; id: string; method: \"set_editor_text\"; text: string };\n\n// ============================================================================\n// Extension UI Commands (stdin)\n// ============================================================================\n\n/** Response to an extension UI request */\nexport type RpcExtensionUIResponse =\n\t| { type: \"extension_ui_response\"; id: string; value: string }\n\t| { type: \"extension_ui_response\"; id: string; confirmed: boolean }\n\t| { type: \"extension_ui_response\"; id: string; cancelled: true };\n\n// ============================================================================\n// Helper type for extracting command types\n// ============================================================================\n\nexport type RpcCommandType = RpcCommand[\"type\"];\n"]}
1
+ {"version":3,"file":"rpc-types.js","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * RPC protocol types for headless operation.\n *\n * Commands are sent as JSON lines on stdin.\n * Responses and events are emitted as JSON lines on stdout.\n */\n\nimport type { AgentMessage, ThinkingLevel } from \"@dreb/agent-core\";\nimport type { ImageContent, Model, Transport } from \"@dreb/ai\";\nimport type { SessionStats } from \"../../core/agent-session.js\";\nimport type { BashResult } from \"../../core/bash-executor.js\";\nimport type { CompactionResult } from \"../../core/compaction/index.js\";\nimport type { ContextUsage } from \"../../core/extensions/types.js\";\nimport type { SessionEntry } from \"../../core/session-manager.js\";\nimport type { SourceInfo } from \"../../core/source-info.js\";\n\n// ============================================================================\n// RPC Commands (stdin)\n// ============================================================================\n\nexport type RpcCommand =\n\t// Prompting\n\t| { id?: string; type: \"prompt\"; message: string; images?: ImageContent[]; streamingBehavior?: \"steer\" | \"followUp\" }\n\t| { id?: string; type: \"steer\"; message: string; images?: ImageContent[] }\n\t| { id?: string; type: \"follow_up\"; message: string; images?: ImageContent[] }\n\t| { id?: string; type: \"abort\" }\n\t| { id?: string; type: \"new_session\"; parentSession?: string }\n\n\t// State\n\t| { id?: string; type: \"get_state\" }\n\t| { id?: string; type: \"get_resources\" }\n\t| { id?: string; type: \"get_git_branch\" }\n\t| { id?: string; type: \"get_daily_cost\" }\n\n\t// Model\n\t| { id?: string; type: \"set_model\"; provider: string; modelId: string }\n\t| { id?: string; type: \"resolve_model\"; pattern: string }\n\t| { id?: string; type: \"cycle_model\" }\n\t| { id?: string; type: \"get_available_models\" }\n\n\t// Buddy — hatch/reroll run inside the agent process so API keys never leave\n\t| { id?: string; type: \"buddy_hatch\" }\n\t| { id?: string; type: \"buddy_reroll\" }\n\n\t// Thinking\n\t| { id?: string; type: \"set_thinking_level\"; level: ThinkingLevel }\n\t| { id?: string; type: \"cycle_thinking_level\" }\n\n\t// Queue modes\n\t| { id?: string; type: \"set_steering_mode\"; mode: \"all\" | \"one-at-a-time\" }\n\t| { id?: string; type: \"set_follow_up_mode\"; mode: \"all\" | \"one-at-a-time\" }\n\t| { id?: string; type: \"get_pending_messages\" }\n\t| { id?: string; type: \"clear_pending_messages\" }\n\n\t// Compaction\n\t| { id?: string; type: \"compact\"; customInstructions?: string }\n\t| { id?: string; type: \"set_auto_compaction\"; enabled: boolean }\n\t| { id?: string; type: \"abort_compaction\" }\n\n\t// Retry\n\t| { id?: string; type: \"set_auto_retry\"; enabled: boolean }\n\t| { id?: string; type: \"abort_retry\" }\n\n\t// Bash\n\t| { id?: string; type: \"bash\"; command: string }\n\t| { id?: string; type: \"abort_bash\" }\n\n\t// Session\n\t| { id?: string; type: \"get_session_stats\" }\n\t| { id?: string; type: \"get_performance_stats\" }\n\t| { id?: string; type: \"export_html\"; outputPath?: string }\n\t| { id?: string; type: \"switch_session\"; sessionPath: string }\n\t| { id?: string; type: \"delete_session\"; sessionPath: string }\n\t| { id?: string; type: \"fork\"; entryId: string }\n\t| { id?: string; type: \"get_fork_messages\" }\n\t| { id?: string; type: \"get_tree\" }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"navigate_tree\";\n\t\t\ttargetId: string;\n\t\t\tsummarize?: boolean;\n\t\t\tcustomInstructions?: string;\n\t\t\treplaceInstructions?: boolean;\n\t\t\tlabel?: string;\n\t }\n\t| { id?: string; type: \"get_last_assistant_text\" }\n\t| { id?: string; type: \"set_session_name\"; name: string }\n\n\t// Messages\n\t| { id?: string; type: \"get_messages\" }\n\n\t// Commands (available for invocation via prompt)\n\t| { id?: string; type: \"get_commands\" }\n\n\t// Session listing\n\t| { id?: string; type: \"list_sessions\" }\n\t| { id?: string; type: \"list_all_sessions\" }\n\n\t// Background agents\n\t| { id?: string; type: \"list_background_agents\" }\n\t| { id?: string; type: \"list_agent_types\" }\n\n\t// Settings (persistent defaults)\n\t| { id?: string; type: \"get_settings\" }\n\t| { id?: string; type: \"set_settings\"; settings: RpcSettingsUpdate }\n\n\t// Version\n\t| { id?: string; type: \"get_version\" };\n\n// ============================================================================\n// RPC Slash Command (for get_commands response)\n// ============================================================================\n\n/** A command available for invocation via prompt */\nexport interface RpcSlashCommand {\n\t/** Command name (without leading slash) */\n\tname: string;\n\t/** Human-readable description */\n\tdescription?: string;\n\t/** What kind of command this is */\n\tsource: \"extension\" | \"prompt\" | \"skill\";\n\t/** Source metadata for the owning resource */\n\tsourceInfo: SourceInfo;\n}\n\nexport interface RpcScopedModel {\n\tprovider: string;\n\tid: string;\n\tname?: string;\n\treasoning?: boolean;\n\tthinkingLevel?: string;\n}\n\nexport interface RpcResources {\n\tcontextFiles: Array<{ path: string }>;\n\tskills: Array<{ name: string; description: string }>;\n\textensions: Array<{ name?: string; path: string }>;\n\tpromptTemplates: Array<{ name: string; description?: string }>;\n\tsystemPromptPresent: boolean;\n}\n\nexport interface RpcQueuedMessage {\n\ttext: string;\n\timages?: ImageContent[];\n}\n\nexport interface RpcPendingMessages {\n\t/** Text-only compatibility view for existing clients. */\n\tsteering: string[];\n\t/** Text-only compatibility view for existing clients. */\n\tfollowUp: string[];\n\t/** Full queued payloads, including inline image attachments. */\n\tsteeringMessages?: RpcQueuedMessage[];\n\t/** Full queued payloads, including inline image attachments. */\n\tfollowUpMessages?: RpcQueuedMessage[];\n}\n\n// ============================================================================\n// RPC State\n// ============================================================================\n\nexport interface RpcSessionState {\n\tmodel?: Model<any>;\n\tscopedModels: RpcScopedModel[];\n\tusingSubscription: boolean;\n\tthinkingLevel: ThinkingLevel;\n\tisStreaming: boolean;\n\tisCompacting: boolean;\n\tsteeringMode: \"all\" | \"one-at-a-time\";\n\tfollowUpMode: \"all\" | \"one-at-a-time\";\n\tsessionFile?: string;\n\tsessionId: string;\n\tsessionName?: string;\n\tautoCompactionEnabled: boolean;\n\tmessageCount: number;\n\tpendingMessageCount: number;\n\t/**\n\t * Context window usage computed by the session — the exact numbers the TUI footer\n\t * renders (AgentSession.getContextUsage()). `tokens`/`percent` are null when usage\n\t * is unknown (e.g. right after compaction, before the next LLM response). Undefined\n\t * when no model is set or the model has no context window.\n\t */\n\tcontextUsage?: ContextUsage;\n\t/** Non-empty when the model was changed from the user's saved preference\n\t * (e.g. saved model unavailable after restart). */\n\tmodelFallbackMessage?: string;\n}\n\n// ============================================================================\n// RPC Responses (stdout)\n// ============================================================================\n\n// Success responses with data\nexport type RpcResponse =\n\t// Prompting (async - events follow)\n\t| { id?: string; type: \"response\"; command: \"prompt\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"steer\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"follow_up\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"abort\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"new_session\"; success: true; data: { cancelled: boolean } }\n\n\t// State\n\t| { id?: string; type: \"response\"; command: \"get_state\"; success: true; data: RpcSessionState }\n\t| { id?: string; type: \"response\"; command: \"get_resources\"; success: true; data: RpcResources }\n\t| { id?: string; type: \"response\"; command: \"get_git_branch\"; success: true; data: { branch: string | null } }\n\t| { id?: string; type: \"response\"; command: \"get_daily_cost\"; success: true; data: { cost: number } }\n\n\t// Model\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"set_model\";\n\t\t\tsuccess: true;\n\t\t\tdata: Model<any>;\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"resolve_model\";\n\t\t\tsuccess: true;\n\t\t\tdata: { model: Model<any>; warning?: string } | null;\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"cycle_model\";\n\t\t\tsuccess: true;\n\t\t\tdata: { model: Model<any>; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_available_models\";\n\t\t\tsuccess: true;\n\t\t\tdata: { models: Model<any>[] };\n\t }\n\n\t// Buddy\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"buddy_hatch\";\n\t\t\tsuccess: true;\n\t\t\tdata: { state: import(\"../../core/buddy/buddy-types.js\").BuddyState };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"buddy_reroll\";\n\t\t\tsuccess: true;\n\t\t\tdata: { state: import(\"../../core/buddy/buddy-types.js\").BuddyState };\n\t }\n\n\t// Thinking\n\t| { id?: string; type: \"response\"; command: \"set_thinking_level\"; success: true }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"cycle_thinking_level\";\n\t\t\tsuccess: true;\n\t\t\tdata: { level: ThinkingLevel } | null;\n\t }\n\n\t// Queue modes\n\t| { id?: string; type: \"response\"; command: \"set_steering_mode\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"set_follow_up_mode\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"get_pending_messages\"; success: true; data: RpcPendingMessages }\n\t| { id?: string; type: \"response\"; command: \"clear_pending_messages\"; success: true; data: RpcPendingMessages }\n\n\t// Compaction\n\t| { id?: string; type: \"response\"; command: \"compact\"; success: true; data: CompactionResult }\n\t| { id?: string; type: \"response\"; command: \"set_auto_compaction\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"abort_compaction\"; success: true }\n\n\t// Retry\n\t| { id?: string; type: \"response\"; command: \"set_auto_retry\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"abort_retry\"; success: true }\n\n\t// Bash\n\t| { id?: string; type: \"response\"; command: \"bash\"; success: true; data: BashResult }\n\t| { id?: string; type: \"response\"; command: \"abort_bash\"; success: true }\n\n\t// Session\n\t| { id?: string; type: \"response\"; command: \"get_session_stats\"; success: true; data: SessionStats }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_performance_stats\";\n\t\t\tsuccess: true;\n\t\t\tdata: { models: Array<{ provider: string; modelId: string; median: number; mean: number; count: number }> };\n\t }\n\t| { id?: string; type: \"response\"; command: \"export_html\"; success: true; data: { path: string } }\n\t| { id?: string; type: \"response\"; command: \"switch_session\"; success: true; data: { cancelled: boolean } }\n\t| { id?: string; type: \"response\"; command: \"delete_session\"; success: true; data: { method: \"trash\" | \"unlink\" } }\n\t| { id?: string; type: \"response\"; command: \"fork\"; success: true; data: { text: string; cancelled: boolean } }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_fork_messages\";\n\t\t\tsuccess: true;\n\t\t\tdata: { messages: Array<{ entryId: string; text: string }> };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_tree\";\n\t\t\tsuccess: true;\n\t\t\tdata: { roots: RpcTreeNode[]; leafId: string | null };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"navigate_tree\";\n\t\t\tsuccess: true;\n\t\t\tdata: { cancelled: boolean; editorText?: string };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_last_assistant_text\";\n\t\t\tsuccess: true;\n\t\t\tdata: { text: string | null };\n\t }\n\t| { id?: string; type: \"response\"; command: \"set_session_name\"; success: true }\n\n\t// Messages\n\t| { id?: string; type: \"response\"; command: \"get_messages\"; success: true; data: { messages: AgentMessage[] } }\n\n\t// Commands\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_commands\";\n\t\t\tsuccess: true;\n\t\t\tdata: { commands: RpcSlashCommand[] };\n\t }\n\n\t// Session listing\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"list_sessions\";\n\t\t\tsuccess: true;\n\t\t\tdata: { sessions: RpcSessionInfo[] };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"list_all_sessions\";\n\t\t\tsuccess: true;\n\t\t\tdata: { sessions: RpcSessionInfo[] };\n\t }\n\n\t// Background agents\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"list_background_agents\";\n\t\t\tsuccess: true;\n\t\t\tdata: { agents: RpcBackgroundAgentInfo[] };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"list_agent_types\";\n\t\t\tsuccess: true;\n\t\t\tdata: { agentTypes: RpcAgentTypeInfo[] };\n\t }\n\n\t// Settings\n\t| { id?: string; type: \"response\"; command: \"get_settings\"; success: true; data: RpcSettingsSnapshot }\n\t| { id?: string; type: \"response\"; command: \"set_settings\"; success: true; data: RpcSettingsSetResult }\n\n\t// Version\n\t| { id?: string; type: \"response\"; command: \"get_version\"; success: true; data: { version: string } }\n\n\t// Error response (any command can fail)\n\t| { id?: string; type: \"response\"; command: string; success: false; error: string };\n\n// ============================================================================\n// Session Info (for list_sessions response)\n// ============================================================================\n\n/** Session metadata returned by list_sessions */\nexport interface RpcSessionInfo {\n\t/** Full path to the session JSONL file */\n\tpath: string;\n\t/** Session UUID */\n\tid: string;\n\t/** Working directory where the session was started */\n\tcwd: string;\n\t/** User-defined display name */\n\tname?: string;\n\t/** ISO timestamp of session creation */\n\tcreated: string;\n\t/** ISO timestamp of last modification */\n\tmodified: string;\n\t/** Number of messages in the session */\n\tmessageCount: number;\n\t/** First user message text */\n\tfirstMessage: string;\n}\n\n/** Background agent metadata returned by list_background_agents */\nexport interface RpcBackgroundAgentInfo {\n\t/** Registry ID (hex) used in background_agent_* events */\n\tagentId: string;\n\t/** Agent type name (e.g. \"Explore\") */\n\tagentType: string;\n\t/** Short human-readable task label */\n\ttaskSummary: string;\n\t/** ISO timestamp of launch */\n\tstartedAt: string;\n\t/** Lifecycle status */\n\tstatus: \"running\" | \"completed\" | \"failed\";\n\t/** Directory containing the agent's session JSONL file (known at spawn time) */\n\tsessionDir?: string;\n\t/** Path to the agent's session JSONL file (available after the child exits) */\n\tsessionFile?: string;\n\t/** Working directory the agent runs in */\n\tcwd?: string;\n}\n\n/** Agent type metadata returned by list_agent_types */\nexport interface RpcAgentTypeInfo {\n\t/** Agent type name, e.g. \"Explore\" */\n\tname: string;\n\t/** Human-readable description from the agent frontmatter */\n\tdescription: string;\n}\n\n/** Serializable session tree node returned by get_tree. Stable DTO — no raw entry payloads. */\nexport interface RpcTreeNode {\n\t/** Entry id */\n\tid: string;\n\t/**\n\t * Parent entry id, or null for a root. Orphaned roots (broken parent chains) keep their\n\t * original non-null parentId, which references an entry not present in the tree — prefer\n\t * the nested `children` structure over parentId when reconstructing hierarchy.\n\t */\n\tparentId: string | null;\n\t/** Session entry type */\n\ttype: SessionEntry[\"type\"];\n\t/** Message role, present only when type === \"message\" (user, assistant, toolResult, bashExecution, ...) */\n\trole?: string;\n\t/** Short single-line content preview (whitespace-collapsed, max 200 chars) */\n\tpreview: string;\n\t/** ISO timestamp of the entry */\n\ttimestamp: string;\n\t/** Resolved label, if any */\n\tlabel?: string;\n\t/** Child nodes, oldest first */\n\tchildren: RpcTreeNode[];\n}\n\n// ============================================================================\n// Settings (persistent defaults)\n// ============================================================================\n\n/**\n * Snapshot of persistent default settings returned by `get_settings` and `set_settings`.\n *\n * These are the values persisted via SettingsManager (merged global + project view) that\n * seed fresh runtimes — NOT the live session state. For the current runtime state\n * (active model, effective thinking level, modes in effect), use `get_state`.\n */\nexport interface RpcSettingsSnapshot {\n\t/** Default provider used at startup (absent if never set) */\n\tdefaultProvider?: string;\n\t/** Default model id used at startup (absent if never set) */\n\tdefaultModel?: string;\n\t/** Default thinking level applied at startup (absent if never set) */\n\tdefaultThinkingLevel?: ThinkingLevel;\n\t/** How queued steering messages are delivered */\n\tsteeringMode: \"all\" | \"one-at-a-time\";\n\t/** How queued follow-up messages are delivered */\n\tfollowUpMode: \"all\" | \"one-at-a-time\";\n\t/** Whether automatic context compaction is enabled */\n\tcompactionEnabled: boolean;\n\t/** Whether automatic retry on transient errors is enabled */\n\tretryEnabled: boolean;\n\t/** Whether image inputs are automatically resized before sending to providers */\n\timageAutoResize?: boolean;\n\t/** Whether image inputs are blocked from being sent to providers */\n\tblockImages?: boolean;\n\t/** Whether skills are registered as slash commands */\n\tenableSkillCommands?: boolean;\n\t/** Whether nested AGENTS.md/CLAUDE.md context auto-loads when tools enter subdirectories */\n\tautoLoadNestedContext?: boolean;\n\t/** Preferred model transport */\n\ttransport?: Transport;\n\t/** Whether raw thinking blocks are hidden in rendered transcripts */\n\thideThinkingBlock?: boolean;\n\t/** Per-agent model fallback lists, merged global + project with project entries winning */\n\tagentModels?: Record<string, string[]>;\n}\n\n/** Settings snapshot returned by `set_settings`; warnings are present for loud shadowing notices. */\nexport type RpcSettingsSetResult = RpcSettingsSnapshot & { warnings?: string[] };\n\n/**\n * Partial update payload for `set_settings`. All fields optional, but at least one\n * must be present. `defaultProvider` and `defaultModel` must be supplied together.\n * Writes persistent defaults only — never touches live session state.\n */\nexport interface RpcSettingsUpdate {\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: ThinkingLevel;\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\tcompactionEnabled?: boolean;\n\tretryEnabled?: boolean;\n\timageAutoResize?: boolean;\n\tblockImages?: boolean;\n\tenableSkillCommands?: boolean;\n\tautoLoadNestedContext?: boolean;\n\ttransport?: Transport;\n\thideThinkingBlock?: boolean;\n\tagentModels?: Record<string, string[]>;\n}\n\n// ============================================================================\n// Extension UI Events (stdout)\n// ============================================================================\n\n/** Emitted when an extension needs user input */\nexport type RpcExtensionUIRequest =\n\t| { type: \"extension_ui_request\"; id: string; method: \"select\"; title: string; options: string[]; timeout?: number }\n\t| { type: \"extension_ui_request\"; id: string; method: \"confirm\"; title: string; message: string; timeout?: number }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"input\";\n\t\t\ttitle: string;\n\t\t\tplaceholder?: string;\n\t\t\ttimeout?: number;\n\t }\n\t| { type: \"extension_ui_request\"; id: string; method: \"editor\"; title: string; prefill?: string }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"notify\";\n\t\t\tmessage: string;\n\t\t\tnotifyType?: \"info\" | \"warning\" | \"error\";\n\t }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"setStatus\";\n\t\t\tstatusKey: string;\n\t\t\tstatusText: string | undefined;\n\t }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"setWidget\";\n\t\t\twidgetKey: string;\n\t\t\twidgetLines: string[] | undefined;\n\t\t\twidgetPlacement?: \"aboveEditor\" | \"belowEditor\";\n\t }\n\t| { type: \"extension_ui_request\"; id: string; method: \"setTitle\"; title: string }\n\t| { type: \"extension_ui_request\"; id: string; method: \"set_editor_text\"; text: string };\n\n// ============================================================================\n// Extension UI Commands (stdin)\n// ============================================================================\n\n/** Response to an extension UI request */\nexport type RpcExtensionUIResponse =\n\t| { type: \"extension_ui_response\"; id: string; value: string }\n\t| { type: \"extension_ui_response\"; id: string; confirmed: boolean }\n\t| { type: \"extension_ui_response\"; id: string; cancelled: true };\n\n// ============================================================================\n// Helper type for extracting command types\n// ============================================================================\n\nexport type RpcCommandType = RpcCommand[\"type\"];\n"]}
@@ -0,0 +1,254 @@
1
+ # Web Dashboard
2
+
3
+ `dreb dashboard` launches a browser UI for dreb: a fleet overview of sessions
4
+ across projects, a full-parity chat view, live background-subagent
5
+ observability, host file browsing, and settings — usable from desktop and
6
+ mobile browsers.
7
+
8
+ The dashboard lives in the `@dreb/dashboard` package. Live agent control goes
9
+ over [RPC mode](rpc.md): the server maintains a pool of `dreb --mode rpc`
10
+ child processes, one per live session. The server uses dreb's public session
11
+ APIs for on-disk inventory/delete and serves its own host file API.
12
+
13
+ ## Launching
14
+
15
+ ```bash
16
+ # via the dreb CLI (requires @dreb/dashboard to be installed)
17
+ dreb dashboard [--port 5343]
18
+
19
+ # or directly
20
+ dreb-dashboard [--port 5343]
21
+
22
+ # remote over Tailscale with HTTPS (PWA + notifications on mobile)
23
+ dreb dashboard --remote --allow you@example.com \
24
+ --https --cert /path/cert.pem --key /path/key.pem
25
+ ```
26
+
27
+ If `@dreb/dashboard` is not installed, `dreb dashboard` fails loudly with
28
+ install instructions (`npm install -g @dreb/dashboard`).
29
+
30
+ Open `http://127.0.0.1:5343` on the same machine.
31
+
32
+ ## Local vs remote — exactly two modes
33
+
34
+ **Local-only (default).** The server binds `127.0.0.1` exclusively. Machines
35
+ on your LAN cannot reach it — packets never arrive at the process. No login or
36
+ pairing. Works without Tailscale installed. This is the right mode for
37
+ same-machine use, including work environments.
38
+
39
+ Requests are additionally validated for loopback `Host`/`Origin` headers, so a
40
+ malicious website cannot drive the dashboard API through DNS rebinding.
41
+
42
+ **Remote (opt-in).** For any access from another device — phone, laptop, even
43
+ on the same LAN — the path is [Tailscale](https://tailscale.com):
44
+
45
+ ```bash
46
+ dreb dashboard --remote --allow you@example.com --allow teammate@example.com
47
+ # or: dreb-dashboard --remote --allow you@example.com --allow teammate@example.com
48
+ ```
49
+
50
+ Layers, in order, all fail-closed (any auth-subsystem error denies):
51
+
52
+ 1. **Tailscale reachability** — the peer address must resolve to a tailnet
53
+ identity via `tailscale status --json`. No Tailscale, no access.
54
+ 2. **Identity allowlist** — `--allow` login names. An empty allowlist denies
55
+ everyone. Rejected identities see a denial page naming the identity.
56
+ 3. **Pairing code** — first login from a new device requires the current
57
+ 6-digit rotating code. The code is shown live in settings → devices on the
58
+ host/local dashboard and rotates every 30 seconds (the current code is also
59
+ printed at server start as a headless fallback). It proves the person can
60
+ see the host machine's local dashboard, so a stolen allowlist identity can't
61
+ quietly gain access.
62
+ 4. **Device cookie** — successful pairing sets a signed HttpOnly cookie
63
+ (30-day expiry). Paired devices are listed in settings → devices and can be
64
+ unpaired at any time.
65
+
66
+ **There is no LAN mode.** The server never listens on a LAN-reachable
67
+ interface without Tailscale identity enforcement.
68
+
69
+ ### What a paired device can do
70
+
71
+ Pairing grants the same power as sitting at the terminal: chatting with
72
+ agents, running commands through them, browsing the whole host filesystem
73
+ (anywhere the dreb process can read), and uploading/downloading files. The
74
+ pairing screen states this before the PIN is entered. Every file operation is
75
+ logged server-side.
76
+
77
+ ## Screens
78
+
79
+ | Screen | What it does |
80
+ |---|---|
81
+ | **Fleet** | Home. Live-first: one grid of every live session at the top — status chip (● running / ◆ needs-attention / ○ idle / ✕ error), project path, activity line, live subagent lines, tasks progress, ctx%, model, last activity. Live cards keep a deterministic order by project path, then session start time; needs-attention cards badge the browser tab without jumping around. Below the grid: past sessions grouped by project, three compact rows per group with an "all N on disk" expander, resume and delete. |
82
+ | **Session view** | Full chat drill-in. Markdown streaming transcript (text, thinking blocks with expand preference, agent-result cards, tool cards with bespoke read/write/edit/bash bodies plus full expandable inputs and markdown-rendered results for markdown-contract tools like subagent/skill/web_fetch/suggest_next, compaction/branch summaries, custom messages), per-message copy, tasks panel, subagent strip, status line with elapsed time plus ■ stop and compaction/retry aborts, and an info bar with cwd, branch, session name, token breakdown, cost/(sub)/daily rollup, ctx%, median tok/s, and a stats popover. Composer supports auto-grow, history, `/` autocomplete from `get_commands`, image attach/paste, queued-message chips with restore-all, steer/follow-up modes, and suggest-next. The ⋯ menu covers export HTML, compact, rename, fork-from-message, loaded context, and tool expand/collapse. Session names update live from manual rename or auto-naming. Extension UI requests (select/confirm/input/editor) render as modals; notifications as toasts. |
83
+ | **Subagent view** | Read-only transcript of a background agent: live events via the RPC relay, hydrated from the agent's on-disk session log (`/subagents/:agentId/messages`) so the view survives browser reloads. Shows the task, streaming output, and tool activity. No composer — subagents can't be steered yet; the parent session controls them. |
84
+ | **Files** | Host-wide browser with places shortcuts (home, /tmp, project roots), breadcrumbs to `/`, new-folder, download, drop-zone/picker upload with explicit collision prompts, and "new session here" on any directory. |
85
+ | **Settings** | Persistent defaults (default model, thinking level, steering/follow-up queue modes, auto-compaction, auto-retry) via `get_settings`/`set_settings` — validation errors are shown verbatim. Dashboard-local preferences (always expand thinking, needs-attention notification permission) live in the browser. Shows the current rotating pairing code on the host/local dashboard, plus the paired-devices list with unpair. |
86
+ | **Pairing** | Remote first-login: identity echo, rotating-code entry, and the security copy explaining what pairing grants. |
87
+
88
+ ### Composer modes
89
+
90
+ While the agent is streaming, the send button becomes mode-aware:
91
+
92
+ - **steer** — deliver now: injected into the running turn after the current
93
+ tool call completes.
94
+ - **follow-up** — queued; delivered after the agent finishes the current work.
95
+ - **■ stop** — abort the current turn. Only visible while streaming.
96
+
97
+ When the agent is idle, send is a plain prompt.
98
+
99
+ ## Notifications
100
+
101
+ Needs-attention notifications are delivered through a **service worker**
102
+ (`registration.showNotification()`), not the page-context `Notification`
103
+ constructor — the constructor was removed from Android Chrome (throws
104
+ `Illegal constructor`) and is absent from iOS Safari entirely. The service
105
+ worker handles `notificationclick`: it focuses an open dashboard client and
106
+ navigates to the session that needs attention, or opens one. All browsers still
107
+ get a `◆` tab-title badge fallback when the tab is hidden.
108
+
109
+ The settings tab exposes a browser-local permission toggle. Gating is unchanged:
110
+ notifications fire only when permission is granted **and** the tab is hidden.
111
+
112
+ **iOS:** notifications exist only in the **installed PWA** (Add to Home Screen,
113
+ iOS 16.4+) — a plain Safari tab has no Notification API regardless of HTTPS.
114
+ The settings copy explains the install prerequisite when it detects an
115
+ un-installed iOS Safari session. (Note: iOS 17.4+ in the EU dropped standalone
116
+ PWA support — installed PWAs open as Safari tabs and push is unavailable there.)
117
+
118
+ ## Installable PWA + secure context
119
+
120
+ The dashboard ships a web app manifest (`display: standalone`, theme/background
121
+ colors, icon set), an apple-touch-icon, and service worker registration, so it
122
+ is **installable to the home screen** on Android Chrome and iOS Safari 16.4+ —
123
+ no URL bar, app-like presence, and (on iOS) the only context where
124
+ notifications work.
125
+
126
+ Service workers and the Notifications API require a **secure context**: HTTPS,
127
+ or `localhost`/`127.0.0.1`. Local mode (`http://127.0.0.1:<port>`) already
128
+ qualifies — install and notifications work with no TLS setup. **Remote mode
129
+ over the tailnet is plain HTTP**, which is not a secure context, so the service
130
+ worker will not register and notifications are unavailable until you enable
131
+ HTTPS. See [Native TLS](#native-tls-remote-https) below.
132
+
133
+ ## Native TLS (remote HTTPS)
134
+
135
+ For PWA install + notifications from a phone over the tailnet, the dashboard
136
+ terminates TLS itself using certificate files from
137
+ [`tailscale cert`](https://tailscale.com/docs/how-to/set-up-https-certificates)
138
+ (no reverse proxy, **no auth-model change**):
139
+
140
+ ```bash
141
+ dreb dashboard --remote --allow you@example.com \
142
+ --https --cert /etc/dreb/cert.pem --key /etc/dreb/key.pem
143
+ ```
144
+
145
+ Because the dashboard terminates TLS directly, `req.socket.remoteAddress` is
146
+ still the phone's real tailnet IP — Tailscale identity resolution, the
147
+ allowlist, and pairing all keep working exactly as in plain-HTTP remote mode.
148
+ There is no header trust, no proxy, no weakening of the auth model.
149
+
150
+ ### One-time cert setup with `tailscale cert`
151
+
152
+ ```bash
153
+ # Enable HTTPS certificates in the Tailscale admin console (DNS → HTTPS) first.
154
+ sudo tailscale cert \
155
+ --cert-file=/etc/dreb/cert.pem \
156
+ --key-file=/etc/dreb/key.pem \
157
+ hostname.tailXXXX.ts.net
158
+ sudo chown dreb:dreb /etc/dreb/cert.pem /etc/dreb/key.pem
159
+ sudo chmod 644 /etc/dreb/cert.pem && sudo chmod 600 /etc/dreb/key.pem
160
+ ```
161
+
162
+ Renewal is **manual** — `tailscale cert` certs are Let's Encrypt, 90-day
163
+ lifetime. The dashboard hot-reloads the cert files on change
164
+ (`setSecureContext`), so a renewal that rewrites the files is picked up with
165
+ zero downtime. A daily systemd timer with `--min-validity=720h` (only renews
166
+ when within 30 days of expiry) is the recommended cadence:
167
+
168
+ ```ini
169
+ # /etc/systemd/system/dreb-cert.service
170
+ [Service]
171
+ Type=oneshot
172
+ ExecStart=/usr/bin/tailscale cert --cert-file=/etc/dreb/cert.pem \
173
+ --key-file=/etc/dreb/key.pem --min-validity=720h hostname.tailXXXX.ts.net
174
+ ExecStartPost=/bin/chown dreb:dreb /etc/dreb/cert.pem /etc/dreb/key.pem
175
+
176
+ # /etc/systemd/system/dreb-cert.timer
177
+ [Timer]
178
+ OnCalendar=daily
179
+ RandomizedDelaySec=3600
180
+ [Install]
181
+ WantedBy=timers.target
182
+ ```
183
+
184
+ Then open `https://hostname.tailXXXX.ts.net:<port>` on the phone.
185
+
186
+ > **Hostname note (important):** the `tailscale cert` certificate is issued
187
+ > for your machine's tailnet name (`hostname.tailXXXX.ts.net`) **only** — not
188
+ > `127.0.0.1`, not a raw tailnet IP. When `--https` is enabled the server
189
+ > speaks TLS on every address it binds, so on the host itself:
190
+ >
191
+ > - `https://hostname.tailXXXX.ts.net:<port>` — works, cert validates (resolves
192
+ > to your tailnet IP). But it's a *remote* request: you go through the full
193
+ > Tailscale allowlist + pairing flow, not instant loopback local mode.
194
+ > - `https://127.0.0.1:<port>` — the server answers, but the browser rejects
195
+ > the cert (no `127.0.0.1` SAN) with a scary warning.
196
+ > - `http://127.0.0.1:<port>` — **dead**: the server only speaks TLS now.
197
+ >
198
+ > If you want the host dashboard tab to stay instant (loopback local mode, no
199
+ > pairing, no warning), run a **second** dashboard process without `--https` on
200
+ > a different port for local-only use, and keep the TLS-enabled one for remote.
201
+ > `--https` is primarily for the `--remote` path; pure-local setups don't need
202
+ > it (`127.0.0.1` is already a secure context).
203
+
204
+ ## Subagent observability
205
+
206
+ Background subagents are first-class:
207
+
208
+ - Fleet cards show running/done counts and live agent lines.
209
+ - The session view shows a chip strip — one chip per background agent; click
210
+ to drill into its live transcript.
211
+ - The drill-in view streams the child's events in real time via the
212
+ `background_agent_event` relay (see [RPC events](rpc.md#event-types)) and
213
+ hydrates from the agent's on-disk session log on mount, so transcripts
214
+ survive browser reloads and remain viewable after the agent finishes.
215
+
216
+ ## Responsive behavior
217
+
218
+ Single breakpoint at 700px. On mobile: fleet cards stack, the session view
219
+ prioritizes read-and-steer (model/thinking switchers collapse into ⋯, tasks
220
+ default collapsed), file table shows name + download only. Composer modes,
221
+ abort, and needs-attention affordances are never reduced away — steering a
222
+ running agent from a phone is the primary remote use case.
223
+
224
+ ## Architecture
225
+
226
+ ```
227
+ Browser dashboard (SolidJS + Vite, tokens.css design system)
228
+ ⇄ Express server: fail-closed auth, REST, SSE fanout, file API
229
+ ⇄ RpcClient pool — one `dreb --mode rpc --ui dashboard` child per session
230
+ ⇄ sessions on disk (~/.dreb/agent/sessions), settings, models
231
+ ```
232
+
233
+ - **SSE catch-up**: events carry sequence IDs; reconnects replay from
234
+ `Last-Event-ID` against a bounded buffer, falling back to a full state
235
+ refetch when the gap is too old. Slow clients whose server-side write
236
+ buffer exceeds a bound are disconnected (loudly logged) and recover via
237
+ the same reconnect path. Deleting a runtime publishes `runtime_removed`
238
+ so browsers evict that session's transcript state.
239
+ - **ctx%** comes from the session itself (`get_state.contextUsage` — the same
240
+ numbers the TUI footer shows), never client-side estimates.
241
+ - **Auto-naming** runs in the shared `AgentSession` layer, so dashboard-created
242
+ RPC sessions get the same LLM-generated session names as the TUI and update
243
+ live via `session_name_changed`.
244
+ - **Visual language**: `tokens.css` (`packages/dashboard/src/client/styles/`),
245
+ the dashboard's design system — IBM Plex Mono, light + dark via
246
+ `prefers-color-scheme`.
247
+
248
+ ## Limitations (deliberate, sequenced later)
249
+
250
+ - No session tree screen yet (fork-from-message covers the go-back-and-re-edit
251
+ loop; the tree design and RPC are ready).
252
+ - No shell passthrough from the browser.
253
+ - No subagent steering (the drill-in view is read-only).
254
+ - Fixed light/dark via `prefers-color-scheme` — no TUI-theme following.
@@ -28,13 +28,16 @@ The script can be run from any directory. dreb keeps the caller's current workin
28
28
 
29
29
  ```
30
30
  packages/
31
- ai/ # Model registry, provider APIs, types (@dreb/ai)
32
- agent/ # Core agent loop, event system, types (@dreb/agent-core)
33
- tui/ # Terminal UI components (@dreb/tui)
34
- coding-agent/ # CLI, tools, interactive mode, TUI app (@dreb/coding-agent)
31
+ ai/ # Model registry, provider APIs, types (@dreb/ai)
32
+ agent/ # Core agent loop, event system, types (@dreb/agent-core)
33
+ tui/ # Terminal UI components (@dreb/tui)
34
+ coding-agent/ # CLI, tools, interactive mode, TUI app (@dreb/coding-agent)
35
+ semantic-search/ # Semantic code search library + MCP server (@dreb/semantic-search)
36
+ telegram/ # Telegram bot frontend over RPC (@dreb/telegram)
37
+ dashboard/ # Web dashboard server/client over RPC (@dreb/dashboard)
35
38
  ```
36
39
 
37
- Dependencies flow one way: `coding-agent` `agent` `ai`, and `coding-agent` `tui`. Changes to a dependency require rebuilding downstream packages — `npm run build` handles this automatically in the correct order.
40
+ Dependencies flow one way through the workspace: `coding-agent` depends on `agent`, `ai`, and `tui`; frontend packages such as `telegram`, `dashboard`, and `semantic-search` build on the published workspace APIs instead of creating reverse dependencies. Changes to a dependency require rebuilding downstream packages — `npm run build` handles this automatically in the correct order.
38
41
 
39
42
  ## Code style
40
43