ompchamber 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/static/css/index.b938f4a693.css +2 -0
  3. package/dist/client/static/js/77.8022a800b5.js +139 -0
  4. package/dist/client/static/js/async/{1k.5974df3b87.js → 1k.a411fe5b43.js} +2 -2
  5. package/dist/client/static/js/async/{3y.71f2ccf3b6.js → 3y.3186ecdd63.js} +5 -5
  6. package/dist/client/static/js/async/5b.d88304bc13.js +1 -0
  7. package/dist/client/static/js/async/66.86544ad741.js +93 -0
  8. package/dist/client/static/js/async/7g.db82aca7ba.js +1 -0
  9. package/dist/client/static/js/async/7l.fadbbc0583.js +1 -0
  10. package/dist/client/static/js/async/{dc.856fda6560.js → ar.ad5c8822a8.js} +2 -2
  11. package/dist/client/static/js/async/cx.9c8268d542.js +226 -0
  12. package/dist/client/static/js/async/e.6443e739a2.js +79 -0
  13. package/dist/client/static/js/async/f7.46971e9d6d.js +2 -0
  14. package/dist/client/static/js/async/g.ee41bfed7f.js +2 -0
  15. package/dist/client/static/js/async/h1.d7091cef78.js +1 -0
  16. package/dist/client/static/js/async/hs.e7c59d8090.js +41 -0
  17. package/dist/client/static/js/async/jl.4911fa3383.js +1 -0
  18. package/dist/client/static/js/async/{k.7466330d1a.js → k.d16cd09ae7.js} +2 -2
  19. package/dist/client/static/js/async/{m.c038c71b9f.js → m.2e1ea3db9e.js} +2 -2
  20. package/dist/client/static/js/async/m3.d2b05f9604.js +2 -0
  21. package/dist/client/static/js/async/mt.853be2b5aa.js +1 -0
  22. package/dist/client/static/js/async/{oc.9c060c9779.js → oc.eb22093f05.js} +1 -1
  23. package/dist/client/static/js/async/{og.634fba99ac.js → og.53f068804c.js} +1 -1
  24. package/dist/client/static/js/async/{5b.0d6ae50fe8.js → q.b453193538.js} +10 -10
  25. package/dist/client/static/js/async/q5.88b50ffa95.js +286 -0
  26. package/dist/client/static/js/async/q8.7f1b48e9cd.js +21 -0
  27. package/dist/client/static/js/async/{sy.d16be843a7.js → s.aad2ecebc6.js} +1 -1
  28. package/dist/client/static/js/async/{tr.e878658140.js → tr.2a0d2b7600.js} +1 -1
  29. package/dist/client/static/js/async/vt.20406a5604.js +1 -0
  30. package/dist/client/static/js/async/x1.fe8af17016.js +90 -0
  31. package/dist/client/static/js/async/yg.5a94e1fc8a.js +149 -0
  32. package/dist/client/static/js/async/yx.af2936c368.js +10 -0
  33. package/dist/client/static/js/index.57b39ffa7f.js +34 -0
  34. package/package.json +1 -1
  35. package/src/client/components/common/ComposerPicker.tsx +16 -4
  36. package/src/client/components/common/ComposerTextarea.tsx +1 -0
  37. package/src/client/components/common/file-icon/index.tsx +4 -1
  38. package/src/client/components/common/image-viewer/index.tsx +224 -0
  39. package/src/client/components/layout/RightActivityBar.tsx +1 -1
  40. package/src/client/components/layout/desktop-layout/ResizeHandle.tsx +2 -2
  41. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +13 -2
  42. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +107 -56
  43. package/src/client/components/layout/desktop-layout/index.tsx +25 -96
  44. package/src/client/components/layout/desktop-layout/resizer/index.tsx +79 -118
  45. package/src/client/components/layout/desktop-layout/resizer/separator-drag.ts +140 -0
  46. package/src/client/components/layout/session-sidebar/Header.tsx +1 -1
  47. package/src/client/components/layout/session-sidebar/SessionItem.tsx +26 -12
  48. package/src/client/components/layout/session-sidebar/SubagentList.tsx +6 -4
  49. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +57 -43
  50. package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +31 -24
  51. package/src/client/components/settings/Modal.tsx +1 -0
  52. package/src/client/components/settings/categories/ChatSettings.tsx +6 -2
  53. package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +99 -0
  54. package/src/client/components/workspace/btw-panel/AskCard.tsx +155 -0
  55. package/src/client/components/workspace/btw-panel/Composer.tsx +181 -0
  56. package/src/client/components/workspace/btw-panel/Messages.tsx +100 -0
  57. package/src/client/components/workspace/btw-panel/TopicMenu.tsx +106 -0
  58. package/src/client/components/workspace/btw-panel/attachments.ts +80 -0
  59. package/src/client/components/workspace/btw-panel/index.tsx +83 -0
  60. package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +39 -20
  61. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +3 -0
  62. package/src/client/components/workspace/chat-timeline/MinimapShortcuts.tsx +49 -12
  63. package/src/client/components/workspace/chat-timeline/SubagentView.tsx +3 -1
  64. package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +10 -6
  65. package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +2 -1
  66. package/src/client/components/workspace/chat-timeline/chat-input/DropOverlay.tsx +17 -0
  67. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +67 -43
  68. package/src/client/components/workspace/chat-timeline/index.tsx +14 -9
  69. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Goal.tsx +3 -3
  70. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +25 -7
  71. package/src/client/components/workspace/editor/Toolbar.tsx +32 -23
  72. package/src/client/components/workspace/editor/index.tsx +45 -36
  73. package/src/client/components/workspace/model-dropdown/groups.ts +35 -2
  74. package/src/client/components/workspace/model-dropdown/index.tsx +48 -7
  75. package/src/client/components/workspace/model-dropdown/use-catalog.ts +28 -4
  76. package/src/client/hooks/chat/btw/index.ts +180 -0
  77. package/src/client/hooks/chat/btw/intercept.ts +43 -0
  78. package/src/client/hooks/chat/btw/mode.ts +179 -0
  79. package/src/client/hooks/chat/composer/attachments.ts +198 -0
  80. package/src/client/hooks/chat/composer/drop-entries.ts +204 -0
  81. package/src/client/hooks/chat/composer/drop-payload.test.ts +97 -0
  82. package/src/client/hooks/chat/composer/drop-payload.ts +130 -0
  83. package/src/client/hooks/chat/composer/drop-references.ts +106 -0
  84. package/src/client/hooks/chat/composer/file-drop.test.ts +194 -0
  85. package/src/client/hooks/chat/composer/file-drop.ts +226 -0
  86. package/src/client/hooks/chat/composer/file-reads.test.ts +211 -0
  87. package/src/client/hooks/chat/composer/file-reads.ts +238 -0
  88. package/src/client/hooks/chat/composer/index.ts +35 -9
  89. package/src/client/hooks/chat/composer/pipeline.ts +181 -0
  90. package/src/client/hooks/chat/omp/index.ts +3 -3
  91. package/src/client/hooks/chat/omp/prompt-send.ts +5 -5
  92. package/src/client/hooks/chat/timeline/actions.ts +40 -4
  93. package/src/client/hooks/chat/timeline/deferred-model.test.ts +219 -0
  94. package/src/client/hooks/chat/timeline/deferred-model.ts +92 -0
  95. package/src/client/hooks/chat/timeline/index.ts +29 -9
  96. package/src/client/hooks/chat/timeline/prompt-text.ts +6 -1
  97. package/src/client/hooks/chat/timeline/send.ts +45 -70
  98. package/src/client/hooks/chat/timeline/session-load.ts +2 -1
  99. package/src/client/hooks/chat/timeline/session-pagination.ts +161 -14
  100. package/src/client/hooks/chat/timeline/user-turns.ts +149 -0
  101. package/src/client/hooks/editor/file-actions.ts +49 -0
  102. package/src/client/hooks/editor/use-file-editor.ts +38 -27
  103. package/src/client/hooks/editor/use-file-transfer.ts +78 -0
  104. package/src/client/hooks/workspace/available-width.ts +33 -0
  105. package/src/client/hooks/workspace/panel-widths.ts +34 -37
  106. package/src/client/tailwind.css +22 -0
  107. package/src/server/db.server.ts +21 -287
  108. package/src/server/lib/btw/frames.test.ts +68 -0
  109. package/src/server/lib/btw/frames.ts +56 -0
  110. package/src/server/lib/btw/prompt.ts +28 -0
  111. package/src/server/lib/btw/registry.server.ts +141 -0
  112. package/src/server/lib/btw/runtime.server.ts +324 -0
  113. package/src/server/lib/btw/schema.server.ts +44 -0
  114. package/src/server/lib/btw/service.server.ts +171 -0
  115. package/src/server/lib/btw/session-copy.server.ts +213 -0
  116. package/src/server/lib/btw/session-copy.test.ts +166 -0
  117. package/src/server/lib/btw/store.server.ts +240 -0
  118. package/src/server/lib/db/schema.ts +110 -0
  119. package/src/server/lib/db/seed.ts +160 -0
  120. package/src/server/lib/db/workspace-sync.ts +70 -0
  121. package/src/server/lib/fs/root.ts +37 -0
  122. package/src/server/lib/models/preferences.server.ts +110 -0
  123. package/src/server/lib/models/registry.server.ts +206 -0
  124. package/src/server/lib/omp/rpc/frame-fold.ts +207 -0
  125. package/src/server/lib/omp/rpc/manager.ts +27 -123
  126. package/src/server/lib/omp/rpc/utility.ts +7 -1
  127. package/src/server/lib/omp/session/auto-title.server.test.ts +137 -0
  128. package/src/server/lib/omp/session/auto-title.server.ts +152 -0
  129. package/src/server/lib/omp/session/blobs.server.test.ts +100 -0
  130. package/src/server/lib/omp/session/blobs.server.ts +76 -0
  131. package/src/server/lib/omp/session/file-scan.ts +174 -0
  132. package/src/server/lib/omp/session/files.test.ts +147 -0
  133. package/src/server/lib/omp/session/files.ts +52 -35
  134. package/src/server/lib/omp/session/locator.ts +2 -2
  135. package/src/server/lib/omp/session/merge-stored.test.ts +118 -0
  136. package/src/server/lib/omp/session/merge-stored.ts +171 -0
  137. package/src/server/lib/omp/session/messages.ts +34 -0
  138. package/src/server/lib/omp/session/reader.ts +2 -2
  139. package/src/server/lib/omp/session/subagent-presence.ts +9 -6
  140. package/src/server/lib/queue/delivery.server.test.ts +260 -0
  141. package/src/server/lib/queue/delivery.server.ts +63 -11
  142. package/src/server/plugins/ssr.ts +29 -2
  143. package/src/server/routes/btw/events.ts +70 -0
  144. package/src/server/routes/btw/index.ts +8 -0
  145. package/src/server/routes/btw/topics.ts +87 -0
  146. package/src/server/routes/btw/ws.ts +136 -0
  147. package/src/server/routes/chat/index.ts +2 -0
  148. package/src/server/routes/chat/session.ts +1 -115
  149. package/src/server/routes/chat/turns.ts +87 -0
  150. package/src/server/routes/fs/index.ts +4 -1
  151. package/src/server/routes/fs/raw.ts +85 -0
  152. package/src/server/routes/fs/read.ts +58 -3
  153. package/src/server/routes/index.ts +7 -1
  154. package/src/server/routes/models/root.ts +88 -215
  155. package/src/server/routes/settings/commands.ts +59 -18
  156. package/src/shared/lib/chat/attachments.test.ts +208 -0
  157. package/src/shared/lib/chat/attachments.ts +200 -15
  158. package/src/shared/lib/chat/btw/client.ts +94 -0
  159. package/src/shared/lib/chat/composer/client.test.ts +48 -0
  160. package/src/shared/lib/chat/composer/client.ts +23 -9
  161. package/src/shared/lib/chat/composer/commands.ts +290 -0
  162. package/src/shared/lib/chat/composer/composer.test.ts +195 -0
  163. package/src/shared/lib/chat/composer/filter.ts +21 -7
  164. package/src/shared/lib/chat/composer/trigger.ts +154 -21
  165. package/src/shared/lib/chat/omp/agent-events.ts +33 -80
  166. package/src/shared/lib/chat/omp/socket.ts +11 -6
  167. package/src/shared/lib/chat/omp/sse.ts +11 -6
  168. package/src/shared/lib/chat/omp/tool-results.ts +102 -0
  169. package/src/shared/lib/chat/omp/transport.ts +19 -3
  170. package/src/shared/lib/chat/text-file.ts +148 -0
  171. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -32
  172. package/src/shared/lib/chat/timeline/stream-coalescer.ts +66 -0
  173. package/src/shared/lib/chat/timeline/turns.ts +7 -0
  174. package/src/shared/lib/fs/file-kind.ts +50 -0
  175. package/src/shared/lib/fs/paths.ts +19 -1
  176. package/src/shared/lib/markdown/math.test.ts +20 -3
  177. package/src/shared/lib/models/preferences.test.ts +131 -0
  178. package/src/shared/lib/models/preferences.ts +88 -0
  179. package/src/shared/lib/models/provider-models.ts +8 -1
  180. package/src/shared/lib/omp/session/inlined-attachments.test.ts +82 -0
  181. package/src/shared/lib/omp/session/mapper.ts +24 -4
  182. package/src/shared/lib/omp/session/messages-map.ts +16 -3
  183. package/src/shared/lib/omp/session/messages-parse.ts +2 -2
  184. package/src/shared/lib/omp/session/parse-message-blocks.ts +85 -5
  185. package/src/shared/lib/omp/subagent/label.test.ts +94 -0
  186. package/src/shared/lib/omp/subagent/label.ts +84 -0
  187. package/src/shared/lib/workspace/panel-widths.test.ts +113 -18
  188. package/src/shared/lib/workspace/panel-widths.ts +161 -30
  189. package/src/shared/lib/workspace/right-panels.ts +50 -11
  190. package/src/shared/lib/workspace/session-state/store.ts +3 -0
  191. package/src/shared/types/btw.ts +65 -0
  192. package/src/shared/types/chat.ts +77 -66
  193. package/src/shared/types/index.ts +2 -0
  194. package/src/shared/types/models.ts +101 -0
  195. package/src/shared/types/omp/agent.ts +14 -1
  196. package/src/shared/types/omp/session.ts +4 -1
  197. package/src/shared/types/omp/subagent.ts +4 -0
  198. package/src/shared/types/settings/command.ts +13 -0
  199. package/src/shared/types/settings/state.ts +5 -0
  200. package/dist/client/static/css/index.e880517fcd.css +0 -2
  201. package/dist/client/static/js/async/5bh.8b2363424f.js +0 -1
  202. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  203. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  204. package/dist/client/static/js/async/7l.7a6a377ecc.js +0 -1
  205. package/dist/client/static/js/async/cx.ca5feec570.js +0 -226
  206. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  207. package/dist/client/static/js/async/g.a06ca3b3e9.js +0 -2
  208. package/dist/client/static/js/async/h1.e8d5aa8e80.js +0 -1
  209. package/dist/client/static/js/async/hs.6d480d0ded.js +0 -41
  210. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  211. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  212. package/dist/client/static/js/async/nam.b5663e919f.js +0 -10
  213. package/dist/client/static/js/async/q.23af2a358f.js +0 -21
  214. package/dist/client/static/js/async/q5.e683f7c26e.js +0 -286
  215. package/dist/client/static/js/async/s.222ee974f6.js +0 -2
  216. package/dist/client/static/js/async/uf.46ad6209fa.js +0 -2
  217. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  218. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  219. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  220. package/dist/client/static/js/index.0de15c5e17.js +0 -33
  221. package/dist/client/static/js/jp.50817896f3.js +0 -139
  222. /package/dist/client/static/js/{jp.50817896f3.js.LICENSE.txt → 77.8022a800b5.js.LICENSE.txt} +0 -0
  223. /package/dist/client/static/js/async/{1k.5974df3b87.js.LICENSE.txt → 1k.a411fe5b43.js.LICENSE.txt} +0 -0
  224. /package/dist/client/static/js/async/{3y.71f2ccf3b6.js.LICENSE.txt → 3y.3186ecdd63.js.LICENSE.txt} +0 -0
  225. /package/dist/client/static/js/async/{dc.856fda6560.js.LICENSE.txt → ar.ad5c8822a8.js.LICENSE.txt} +0 -0
  226. /package/dist/client/static/js/async/{m.c038c71b9f.js.LICENSE.txt → f7.46971e9d6d.js.LICENSE.txt} +0 -0
  227. /package/dist/client/static/js/async/{5b.0d6ae50fe8.js.LICENSE.txt → g.ee41bfed7f.js.LICENSE.txt} +0 -0
  228. /package/dist/client/static/js/async/{q.23af2a358f.js.LICENSE.txt → m.2e1ea3db9e.js.LICENSE.txt} +0 -0
  229. /package/dist/client/static/js/async/{s.222ee974f6.js.LICENSE.txt → m3.d2b05f9604.js.LICENSE.txt} +0 -0
  230. /package/dist/client/static/js/async/{g.a06ca3b3e9.js.LICENSE.txt → q.b453193538.js.LICENSE.txt} +0 -0
  231. /package/dist/client/static/js/async/{uf.46ad6209fa.js.LICENSE.txt → q8.7f1b48e9cd.js.LICENSE.txt} +0 -0
  232. /package/dist/client/static/js/async/{nam.b5663e919f.js.LICENSE.txt → yx.af2936c368.js.LICENSE.txt} +0 -0
  233. /package/dist/client/static/js/{index.0de15c5e17.js.LICENSE.txt → index.57b39ffa7f.js.LICENSE.txt} +0 -0
@@ -33,6 +33,15 @@ export function SessionItem({
33
33
  onToggleExpand,
34
34
  }: SessionItemProps) {
35
35
  const showChevron = Boolean(expandable && hasSubagents && onToggleExpand);
36
+ // The slot shows one glyph, never two side by side. A live run signal owns
37
+ // it at rest — the spinner, or the blocked-on-a-question cue, which says it
38
+ // is YOUR turn. With nothing live to report the chevron is PINNED, collapsed
39
+ // or expanded: it is the only affordance left worth showing, and making it
40
+ // hover-only would leave the roster reachable only by hovering exactly the
41
+ // right 16px.
42
+ const liveRun = awaitingInput || status === 'stream';
43
+ const chevronPinned = showChevron && !liveRun;
44
+ const showStatus = Boolean(awaitingInput || status) && !chevronPinned;
36
45
  const {
37
46
  isEditing,
38
47
  draft,
@@ -52,11 +61,16 @@ export function SessionItem({
52
61
  : 'text-ink/75 hover:text-ink hover:bg-ink/5'
53
62
  }`}
54
63
  >
55
- {/* One 16px slot owns both icons: the run-status indicator (spinner /
56
- check) shows at rest, and the expand toggle takes the slot over on
57
- hover — or permanently while the roster is open. Never two icons.
58
- The toggle is pointer-inert while invisible so the slot keeps
59
- selecting the session until the chevron is what you see. */}
64
+ {/* One 16px slot owns both icons — never two side by side. A live run
65
+ signal (blocked question, run spinner) holds the slot; otherwise the
66
+ chevron is pinned, collapsed or expanded. While the signal holds it,
67
+ hovering hands the slot to the chevron so the roster stays reachable
68
+ mid-run.
69
+ Both layers are pointer-inert for the opposite state: the toggle
70
+ while hidden, the status glyph ALWAYS (it is painted after the
71
+ toggle, so without that it wins the hit test over the revealed
72
+ chevron and every click on a badged row selects the session instead
73
+ of expanding it). */}
60
74
  <span className="relative w-4 h-4 shrink-0">
61
75
  {showChevron && onToggleExpand && (
62
76
  <button
@@ -67,18 +81,18 @@ export function SessionItem({
67
81
  }}
68
82
  title={isExpanded ? 'Collapse subagents' : 'Expand subagents'}
69
83
  aria-expanded={isExpanded}
70
- className={`peer absolute inset-0 flex items-center justify-center rounded cursor-pointer text-ink/40 transition-opacity hover:text-ink ${
71
- isExpanded
72
- ? ''
73
- : 'pointer-events-none opacity-0 group-hover/item:pointer-events-auto group-hover/item:opacity-100 focus-visible:opacity-100'
84
+ className={`peer absolute inset-0 flex items-center justify-center rounded cursor-pointer transition-opacity ${
85
+ chevronPinned
86
+ ? 'text-ink/60 hover:text-ink'
87
+ : 'text-ink/40 hover:text-ink pointer-events-none opacity-0 group-hover/item:pointer-events-auto group-hover/item:opacity-100 focus-visible:opacity-100'
74
88
  }`}
75
89
  >
76
- {isExpanded ? <ChevronDown size={13} className="text-ink/70" /> : <ChevronRight size={13} />}
90
+ {isExpanded ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
77
91
  </button>
78
92
  )}
79
- {(awaitingInput || status) && !(showChevron && isExpanded) && (
93
+ {showStatus && (
80
94
  <span
81
- className={`absolute inset-0 flex items-center justify-center text-ink/60 transition-opacity ${
95
+ className={`pointer-events-none absolute inset-0 flex items-center justify-center text-ink/60 transition-opacity ${
82
96
  showChevron ? 'group-hover/item:opacity-0 peer-focus-visible:opacity-0' : ''
83
97
  }`}
84
98
  >
@@ -3,6 +3,7 @@ import { useSearchParams } from '@/client/lib/router/search-params';
3
3
  import { SubagentStatusIcon } from '@/client/components/common/SubagentStatusIcon';
4
4
  import { isRecord } from '@/shared/lib/util/guards';
5
5
  import { fetchSubagentHistory, historyEntryToSubagentInfo } from '@/shared/lib/omp/subagent/history/client';
6
+ import { subagentRowLabel } from '@/shared/lib/omp/subagent/label';
6
7
  import { mergeSubagentRoster, parseSubagentLifecycle, parseSubagentProgress, parseSubagentRosterResponse } from '@/shared/lib/omp/subagent/parse';
7
8
  import type { SubagentInfo, SubagentProgress } from '@/shared/types';
8
9
 
@@ -123,13 +124,14 @@ export function SubagentList({ sessionId, isActiveSession }: SubagentListProps)
123
124
  </div>
124
125
  ) : (
125
126
  subagents.map((subagent) => {
126
- const taskText = subagent.task ?? subagent.description ?? subagent.assignment ?? subagent.agent ?? '';
127
+ const label = subagentRowLabel(subagent);
128
+ const brief = subagent.task ?? subagent.assignment ?? subagent.description ?? label;
127
129
  const isViewed = subagent.id === viewedSubagentId;
128
130
  return (
129
131
  <button
130
132
  key={subagent.id}
131
133
  type="button"
132
- title={taskText}
134
+ title={brief}
133
135
  onClick={() => window.dispatchEvent(new CustomEvent('omp:view-subagent', { detail: { sessionId: String(sessionId), subagent } }))}
134
136
  className={`w-full flex items-center text-left text-xs rounded-md px-2 py-1 cursor-pointer transition-colors select-none group/subagent ${isViewed ? 'bg-ink/10 font-medium text-ink' : 'text-ink/70 hover:text-ink hover:bg-ink/5'}`}
135
137
  >
@@ -141,8 +143,8 @@ export function SubagentList({ sessionId, isActiveSession }: SubagentListProps)
141
143
  {/* Gap between subagent icon and task text */}
142
144
  <span className="w-2 shrink-0" />
143
145
 
144
- {/* Subagent task description text */}
145
- <span className="flex-1 min-w-0 truncate leading-snug">{taskText}</span>
146
+ {/* Subagent label: `id (agent): target` */}
147
+ <span className="flex-1 min-w-0 truncate leading-snug">{label}</span>
146
148
  </button>
147
149
  );
148
150
  })
@@ -2,6 +2,7 @@ import { useState } from 'preact/hooks';
2
2
  import { AlertTriangle, Loader2 } from 'lucide-preact';
3
3
  import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
4
4
  import { CodeSurface } from '@/client/components/common/code-surface';
5
+ import { ImageViewer } from '@/client/components/common/image-viewer';
5
6
  import { EditorHeader } from '@/client/components/mobile/mobile-right-sidebar/Header';
6
7
  import { useScrollbarFade, scrollbarFadeClass } from '@/client/hooks/ui/scrollbar-fade';
7
8
  import { useFileEditor } from '@/client/hooks/editor/use-file-editor';
@@ -48,6 +49,7 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
48
49
  isPreview={isPreview}
49
50
  wordWrap={wordWrap}
50
51
  copied={editor.copied}
52
+ isImage={editor.isImage}
51
53
  onClose={onClose}
52
54
  onTogglePreview={() => setIsPreview(!isPreview)}
53
55
  onToggleWrap={() => setWordWrap(!wordWrap)}
@@ -57,46 +59,52 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
57
59
  onDownload={editor.download}
58
60
  />
59
61
 
60
- {/* Editor Content Area - Desktop styled: bg-paper text-ink */}
61
- <div onScroll={handleScroll} className={`flex-1 min-h-0 overflow-auto relative bg-paper text-ink select-text ${scrollbarFadeClass(isScrolling)}`}>
62
- {editor.isLoading ? (
63
- <div className="flex items-center justify-center h-full text-ink/40 text-xs gap-2">
64
- <Loader2 size={14} className="animate-spin" />
65
- <span>Loading file content...</span>
66
- </div>
67
- ) : editor.loadError ? (
68
- <div className="flex flex-col items-center justify-center h-full gap-2 px-6 text-center">
69
- <AlertTriangle size={20} className="text-error" />
70
- <span className="text-xs text-error font-sans">Could not read {file.name}</span>
71
- <span className="text-[11px] text-ink/50 font-mono break-all">{editor.loadError}</span>
72
- </div>
73
- ) : isMd && isPreview ? (
74
- /* Markdown Preview Mode */
75
- <div className="p-4 bg-paper text-ink min-h-full font-sans prose prose-sm max-w-none">
76
- <MarkdownRenderer content={content} />
77
- </div>
78
- ) : (
79
- /* Code Editor with Line Numbers - Desktop styled: gutter bg-canvas border-r border-ink/10 */
80
- <CodeSurface
81
- value={content}
82
- onValueChange={editor.onChange}
83
- language={lang}
84
- wordWrap={wordWrap}
85
- rootClassName="flex min-h-full bg-paper"
86
- gutterClassName="w-10 py-3 pr-2 select-none text-right text-[10px] text-ink/30 bg-canvas border-r border-ink/10 font-mono leading-[20px] flex-shrink-0"
87
- editorWrapperClassName="flex-1 p-3 overflow-x-auto min-w-0 bg-paper text-ink code-surface"
88
- editorClassName="focus:outline-none"
89
- editorStyle={{
90
- fontFamily: 'monospace',
91
- fontSize: `${fontSize}px`,
92
- lineHeight: '20px',
93
- backgroundColor: 'transparent',
94
- minHeight: '100%',
95
- color: 'var(--theme-ink)',
96
- }}
97
- />
98
- )}
99
- </div>
62
+ {/* Editor Content Area - Desktop styled: bg-paper text-ink. An image owns
63
+ its own pan/zoom, so it replaces the scrolling text container rather
64
+ than living inside it. */}
65
+ {editor.isImage && editor.imageUrl ? (
66
+ <ImageViewer src={editor.imageUrl} name={file.name} className="flex-1 min-h-0" />
67
+ ) : (
68
+ <div onScroll={handleScroll} className={`flex-1 min-h-0 overflow-auto relative bg-paper text-ink select-text ${scrollbarFadeClass(isScrolling)}`}>
69
+ {editor.isLoading ? (
70
+ <div className="flex items-center justify-center h-full text-ink/40 text-xs gap-2">
71
+ <Loader2 size={14} className="animate-spin" />
72
+ <span>Loading file content...</span>
73
+ </div>
74
+ ) : editor.loadError ? (
75
+ <div className="flex flex-col items-center justify-center h-full gap-2 px-6 text-center">
76
+ <AlertTriangle size={20} className="text-error" />
77
+ <span className="text-xs text-error font-sans">Could not read {file.name}</span>
78
+ <span className="text-[11px] text-ink/50 font-mono break-all">{editor.loadError}</span>
79
+ </div>
80
+ ) : isMd && isPreview ? (
81
+ /* Markdown Preview Mode */
82
+ <div className="p-4 bg-paper text-ink min-h-full font-sans prose prose-sm max-w-none">
83
+ <MarkdownRenderer content={content} />
84
+ </div>
85
+ ) : (
86
+ /* Code Editor with Line Numbers - Desktop styled: gutter bg-canvas border-r border-ink/10 */
87
+ <CodeSurface
88
+ value={content}
89
+ onValueChange={editor.onChange}
90
+ language={lang}
91
+ wordWrap={wordWrap}
92
+ rootClassName="flex min-h-full bg-paper"
93
+ gutterClassName="w-10 py-3 pr-2 select-none text-right text-[10px] text-ink/30 bg-canvas border-r border-ink/10 font-mono leading-[20px] flex-shrink-0"
94
+ editorWrapperClassName="flex-1 p-3 overflow-x-auto min-w-0 bg-paper text-ink code-surface"
95
+ editorClassName="focus:outline-none"
96
+ editorStyle={{
97
+ fontFamily: 'monospace',
98
+ fontSize: `${fontSize}px`,
99
+ lineHeight: '20px',
100
+ backgroundColor: 'transparent',
101
+ minHeight: '100%',
102
+ color: 'var(--theme-ink)',
103
+ }}
104
+ />
105
+ )}
106
+ </div>
107
+ )}
100
108
 
101
109
  {/* Bottom Status Bar - Desktop styled: bg-canvas border-t border-ink/10 */}
102
110
  <footer
@@ -109,9 +117,15 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
109
117
  }}
110
118
  >
111
119
  <div className="flex items-center space-x-3">
112
- <span>{linesCount} lines</span>
113
- <span>{content.length} chars</span>
114
- <span className="uppercase">{lang}</span>
120
+ {editor.isImage ? (
121
+ <span className="uppercase">Image</span>
122
+ ) : (
123
+ <>
124
+ <span>{linesCount} lines</span>
125
+ <span>{content.length} chars</span>
126
+ <span className="uppercase">{lang}</span>
127
+ </>
128
+ )}
115
129
  </div>
116
130
  <div className="flex items-center space-x-2">
117
131
  {editor.saveStatus === 'saving' && (
@@ -8,6 +8,8 @@ interface EditorHeaderProps {
8
8
  isPreview: boolean;
9
9
  wordWrap: boolean;
10
10
  copied: boolean;
11
+ /** Raster image: wrap and font zoom are text-only, so both are hidden. */
12
+ isImage?: boolean;
11
13
  onClose: () => void;
12
14
  onTogglePreview: () => void;
13
15
  onToggleWrap: () => void;
@@ -24,6 +26,7 @@ export function EditorHeader({
24
26
  isPreview,
25
27
  wordWrap,
26
28
  copied,
29
+ isImage = false,
27
30
  onClose,
28
31
  onTogglePreview,
29
32
  onToggleWrap,
@@ -71,32 +74,36 @@ export function EditorHeader({
71
74
  </button>
72
75
  )}
73
76
 
74
- <button
75
- type="button"
76
- onClick={onToggleWrap}
77
- className={`p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer ${wordWrap ? 'text-ink' : ''}`}
78
- title="Toggle word wrap"
79
- >
80
- <WrapText size={15} />
81
- </button>
77
+ {!isImage && (
78
+ <>
79
+ <button
80
+ type="button"
81
+ onClick={onToggleWrap}
82
+ className={`p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer ${wordWrap ? 'text-ink' : ''}`}
83
+ title="Toggle word wrap"
84
+ >
85
+ <WrapText size={15} />
86
+ </button>
82
87
 
83
- <button
84
- type="button"
85
- onClick={onZoomOut}
86
- className="p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer"
87
- title="Decrease font size"
88
- >
89
- <ZoomOut size={15} />
90
- </button>
88
+ <button
89
+ type="button"
90
+ onClick={onZoomOut}
91
+ className="p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer"
92
+ title="Decrease font size"
93
+ >
94
+ <ZoomOut size={15} />
95
+ </button>
91
96
 
92
- <button
93
- type="button"
94
- onClick={onZoomIn}
95
- className="p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer"
96
- title="Increase font size"
97
- >
98
- <ZoomIn size={15} />
99
- </button>
97
+ <button
98
+ type="button"
99
+ onClick={onZoomIn}
100
+ className="p-1.5 rounded hover:bg-ink/10 transition-colors cursor-pointer"
101
+ title="Increase font size"
102
+ >
103
+ <ZoomIn size={15} />
104
+ </button>
105
+ </>
106
+ )}
100
107
 
101
108
  <button
102
109
  type="button"
@@ -40,6 +40,7 @@ const DEFAULT_SETTINGS: SettingsState = {
40
40
  editorFont: 'JetBrains Mono',
41
41
  streamResponses: true,
42
42
  streamTransport: 'websocket',
43
+ autoSessionTitle: true,
43
44
  expandedThinking: true,
44
45
  detailedToolCalls: false,
45
46
  notificationsEnabled: true,
@@ -1,6 +1,7 @@
1
1
  import type { SettingsState } from '@/shared/types';
2
2
  import { ChatFollowUpSection } from '@/client/components/settings/categories/chat-settings/FollowUpSection';
3
3
  import { ChatKeybindingsSection } from '@/client/components/settings/categories/chat-settings/KeybindingsSection';
4
+ import { ChatTitleSection } from '@/client/components/settings/categories/chat-settings/TitleSection';
4
5
  import { ChatTransportSection } from '@/client/components/settings/categories/chat-settings/TransportSection';
5
6
 
6
7
  interface ChatSettingsProps {
@@ -14,10 +15,13 @@ export function ChatSettings({ settings, onUpdate }: ChatSettingsProps) {
14
15
  {/* 1. Streaming Transport (WebSocket vs SSE) */}
15
16
  <ChatTransportSection settings={settings} onUpdate={onUpdate} />
16
17
 
17
- {/* 2. Follow-up Behavior (Interactive Selector Cards) */}
18
+ {/* 2. Session Titles (automatic generation) */}
19
+ <ChatTitleSection settings={settings} onUpdate={onUpdate} />
20
+
21
+ {/* 3. Follow-up Behavior (Interactive Selector Cards) */}
18
22
  <ChatFollowUpSection settings={settings} onUpdate={onUpdate} />
19
23
 
20
- {/* 3. Keyboard Shortcuts Configurator */}
24
+ {/* 4. Keyboard Shortcuts Configurator */}
21
25
  <ChatKeybindingsSection settings={settings} onUpdate={onUpdate} />
22
26
  </div>
23
27
  );
@@ -0,0 +1,99 @@
1
+ import { Check, Sparkles, Type } from 'lucide-preact';
2
+ import type { SettingsState } from '@/shared/types';
3
+
4
+ interface ChatTitleSectionProps {
5
+ settings: SettingsState;
6
+ onUpdate: (updater: Partial<SettingsState> | ((prev: SettingsState) => SettingsState)) => void;
7
+ }
8
+
9
+ const OPTIONS: Array<{
10
+ enabled: boolean;
11
+ label: string;
12
+ badge: string;
13
+ icon: typeof Sparkles;
14
+ description: string;
15
+ note: string;
16
+ }> = [
17
+ {
18
+ enabled: true,
19
+ label: 'Generate Automatically',
20
+ badge: 'Default',
21
+ icon: Sparkles,
22
+ description:
23
+ 'After the first run settles, the session is named from the conversation using omp\'s own tiny title model. Sessions stop showing "New Session - <timestamp>" in the sidebar.',
24
+ note: 'One small model call per session',
25
+ },
26
+ {
27
+ enabled: false,
28
+ label: 'Manual Only',
29
+ badge: 'Opt-out',
30
+ icon: Type,
31
+ description:
32
+ 'Sessions keep their timestamp placeholder until you rename them from the sidebar. Choose this when every title should be deliberate, or to avoid the extra model call.',
33
+ note: 'No background model calls',
34
+ },
35
+ ];
36
+
37
+ export function ChatTitleSection({ settings, onUpdate }: ChatTitleSectionProps) {
38
+ const active = settings.autoSessionTitle ?? true;
39
+
40
+ return (
41
+ <section className="space-y-3">
42
+ <div>
43
+ <h4 className="text-sm font-semibold text-ink">Session Titles</h4>
44
+ <p className="text-[11px] text-ink/60 mt-0.5">
45
+ How a session gets its display name. A name you set yourself is never replaced.
46
+ </p>
47
+ </div>
48
+
49
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
50
+ {OPTIONS.map(({ enabled, label, badge, icon: Icon, description, note }) => {
51
+ const selected = active === enabled;
52
+ const select = () => onUpdate({ autoSessionTitle: enabled });
53
+ return (
54
+ <div
55
+ key={label}
56
+ onClick={select}
57
+ role="button"
58
+ tabIndex={0}
59
+ onKeyDown={(e) => e.key === 'Enter' && select()}
60
+ className={`p-3.5 rounded-lg border text-left transition-all cursor-pointer flex flex-col justify-between select-none ${
61
+ selected
62
+ ? 'border-ink bg-ink/5 ring-1 ring-ink/20 shadow-xs'
63
+ : 'border-ink/15 bg-paper hover:border-ink/30 hover:bg-ink/[0.02]'
64
+ }`}
65
+ >
66
+ <div>
67
+ <div className="flex items-center justify-between mb-2">
68
+ <div className="flex items-center gap-2">
69
+ <div
70
+ className={`w-7 h-7 rounded-md flex items-center justify-center ${
71
+ selected ? 'bg-ink text-paper' : 'bg-ink/5 text-ink/70'
72
+ }`}
73
+ >
74
+ <Icon size={15} />
75
+ </div>
76
+ <span className="font-semibold text-xs text-ink">{label}</span>
77
+ </div>
78
+ <span className="text-[10px] font-mono uppercase px-1.5 py-0.5 rounded bg-ink/10 text-ink/70 font-medium">
79
+ {badge}
80
+ </span>
81
+ </div>
82
+ <p className="text-[11px] text-ink/70 leading-relaxed">{description}</p>
83
+ </div>
84
+
85
+ <div className="mt-3 pt-2.5 border-t border-ink/10 flex items-center justify-between text-[11px]">
86
+ <span className="text-ink/50">{note}</span>
87
+ {selected && (
88
+ <span className="flex items-center gap-1 text-ink font-medium">
89
+ <Check size={12} /> Active
90
+ </span>
91
+ )}
92
+ </div>
93
+ </div>
94
+ );
95
+ })}
96
+ </div>
97
+ </section>
98
+ );
99
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The ask card — the top card of the side-question form.
8
+ *
9
+ * It is the form's own entry, not the chat's: the question typed here is always
10
+ * a question of its own (a new topic), which is why it carries its own `⟳`
11
+ * (start over) and `✕` (leave the form). The topic history and promotion ride
12
+ * along here because the form replaced the composer that used to hold them.
13
+ */
14
+
15
+ import { useEffect, useRef, useState } from 'preact/hooks';
16
+ import type { TargetedKeyboardEvent } from 'preact';
17
+ import { ChevronDown, ExternalLink, RefreshCw, X } from 'lucide-preact';
18
+ import type { BtwTopic } from '@/shared/types';
19
+ import { BtwTopicMenu } from '@/client/components/workspace/btw-panel/TopicMenu';
20
+
21
+ export interface BtwAskCardProps {
22
+ value: string;
23
+ onChange: (value: string) => void;
24
+ onSubmit: () => void;
25
+ /** `⟳` — clear the question and fall back to the newest topic. */
26
+ onNewQuestion: () => void;
27
+ /** `✕` — leave the side-question form. */
28
+ onClose: () => void;
29
+ /** A side answer is streaming: starting over waits for it. */
30
+ running: boolean;
31
+ topics: BtwTopic[];
32
+ activeTopicId: string | null;
33
+ onSelectTopic: (id: string) => void;
34
+ onDeleteTopic: (id: string) => void;
35
+ canPromote: boolean;
36
+ onPromote: () => void;
37
+ }
38
+
39
+ const GHOST_BUTTON =
40
+ 'p-1.5 rounded text-ink/45 hover:text-ink hover:bg-ink/5 transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-ink/45';
41
+
42
+ export function BtwAskCard({
43
+ value,
44
+ onChange,
45
+ onSubmit,
46
+ onNewQuestion,
47
+ onClose,
48
+ running,
49
+ topics,
50
+ activeTopicId,
51
+ onSelectTopic,
52
+ onDeleteTopic,
53
+ canPromote,
54
+ onPromote,
55
+ }: BtwAskCardProps) {
56
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
57
+ const [menuOpen, setMenuOpen] = useState(false);
58
+
59
+ // Grow with the text and collapse again once it is submitted or cleared.
60
+ useEffect(() => {
61
+ const el = textareaRef.current;
62
+ if (!el) return;
63
+ el.style.height = 'auto';
64
+ el.style.height = `${el.scrollHeight}px`;
65
+ }, [value]);
66
+
67
+ const handleKeyDown = (event: TargetedKeyboardEvent<HTMLTextAreaElement>) => {
68
+ if (event.key === 'Escape') {
69
+ event.preventDefault();
70
+ onClose();
71
+ return;
72
+ }
73
+ if (event.key !== 'Enter' || event.shiftKey) return;
74
+ if (event.isComposing || event.keyCode === 229) return;
75
+ event.preventDefault();
76
+ onSubmit();
77
+ };
78
+
79
+ return (
80
+ <div className="bg-paper border border-ink/20 rounded-2xl shadow-sm flex items-center gap-1 pl-1.5 pr-2 py-1 focus-within:border-ink transition-colors">
81
+ <button
82
+ type="button"
83
+ onClick={() => {
84
+ onNewQuestion();
85
+ // The button took the focus; asking again means typing again.
86
+ textareaRef.current?.focus();
87
+ }}
88
+ disabled={running}
89
+ className={GHOST_BUTTON}
90
+ aria-label="New side question"
91
+ title="New side question"
92
+ >
93
+ <RefreshCw size={14} strokeWidth={2} />
94
+ </button>
95
+
96
+ <textarea
97
+ ref={textareaRef}
98
+ rows={1}
99
+ value={value}
100
+ onInput={(event) => onChange(event.currentTarget.value)}
101
+ onKeyDown={handleKeyDown}
102
+ placeholder="Ask your question"
103
+ aria-label="Ask your question"
104
+ className="flex-1 min-w-0 bg-transparent text-[13px] text-ink placeholder-ink/40 resize-none outline-none py-1.5 max-h-32 leading-6"
105
+ />
106
+
107
+ {topics.length > 0 && (
108
+ <div className="relative">
109
+ <button
110
+ type="button"
111
+ onClick={() => setMenuOpen((open) => !open)}
112
+ className={GHOST_BUTTON}
113
+ aria-label="Side question history"
114
+ title="Side question history"
115
+ aria-expanded={menuOpen}
116
+ data-btw-menu-anchor=""
117
+ >
118
+ <ChevronDown size={14} strokeWidth={2} />
119
+ </button>
120
+ {menuOpen && (
121
+ <BtwTopicMenu
122
+ topics={topics}
123
+ activeTopicId={activeTopicId}
124
+ onSelectTopic={onSelectTopic}
125
+ onDeleteTopic={onDeleteTopic}
126
+ onCloseMenu={() => setMenuOpen(false)}
127
+ />
128
+ )}
129
+ </div>
130
+ )}
131
+
132
+ {canPromote && (
133
+ <button
134
+ type="button"
135
+ onClick={onPromote}
136
+ className={GHOST_BUTTON}
137
+ aria-label="Promote answer into chat"
138
+ title="Promote answer into chat"
139
+ >
140
+ <ExternalLink size={14} strokeWidth={2} />
141
+ </button>
142
+ )}
143
+
144
+ <button
145
+ type="button"
146
+ onClick={onClose}
147
+ className={GHOST_BUTTON}
148
+ aria-label="Close side questions"
149
+ title="Close side questions"
150
+ >
151
+ <X size={14} strokeWidth={2} />
152
+ </button>
153
+ </div>
154
+ );
155
+ }