ompchamber 1.0.0 → 1.1.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 (64) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/static/css/index.b859cc1cc5.css +2 -0
  3. package/dist/client/static/js/async/{5b.0d6ae50fe8.js → 5b.a5ccbc19dc.js} +2 -2
  4. package/dist/client/static/js/index.6e065017eb.js +33 -0
  5. package/package.json +1 -1
  6. package/src/client/components/common/ComposerPicker.tsx +16 -4
  7. package/src/client/components/common/ComposerTextarea.tsx +1 -0
  8. package/src/client/components/layout/RightActivityBar.tsx +1 -1
  9. package/src/client/components/layout/desktop-layout/ResizeHandle.tsx +2 -2
  10. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +13 -2
  11. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +107 -56
  12. package/src/client/components/layout/desktop-layout/index.tsx +25 -96
  13. package/src/client/components/layout/desktop-layout/resizer/index.tsx +79 -118
  14. package/src/client/components/layout/desktop-layout/resizer/separator-drag.ts +140 -0
  15. package/src/client/components/layout/session-sidebar/Header.tsx +1 -1
  16. package/src/client/components/layout/session-sidebar/SessionItem.tsx +26 -12
  17. package/src/client/components/layout/session-sidebar/SubagentList.tsx +6 -4
  18. package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +3 -0
  19. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +3 -0
  20. package/src/client/components/workspace/chat-timeline/SubagentView.tsx +3 -1
  21. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +23 -3
  22. package/src/client/components/workspace/chat-timeline/index.tsx +3 -0
  23. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Goal.tsx +3 -3
  24. package/src/client/components/workspace/model-dropdown/groups.ts +35 -2
  25. package/src/client/components/workspace/model-dropdown/index.tsx +48 -7
  26. package/src/client/components/workspace/model-dropdown/use-catalog.ts +28 -4
  27. package/src/client/hooks/chat/composer/index.ts +28 -8
  28. package/src/client/hooks/chat/timeline/actions.ts +24 -3
  29. package/src/client/hooks/chat/timeline/deferred-model.test.ts +219 -0
  30. package/src/client/hooks/chat/timeline/deferred-model.ts +92 -0
  31. package/src/client/hooks/chat/timeline/index.ts +13 -1
  32. package/src/client/hooks/chat/timeline/send.ts +16 -5
  33. package/src/client/hooks/workspace/available-width.ts +33 -0
  34. package/src/client/hooks/workspace/panel-widths.ts +34 -37
  35. package/src/server/lib/models/preferences.server.ts +110 -0
  36. package/src/server/lib/models/registry.server.ts +206 -0
  37. package/src/server/lib/omp/rpc/utility.ts +7 -1
  38. package/src/server/lib/queue/delivery.server.test.ts +203 -0
  39. package/src/server/lib/queue/delivery.server.ts +38 -8
  40. package/src/server/routes/models/root.ts +88 -215
  41. package/src/server/routes/settings/commands.ts +59 -18
  42. package/src/shared/lib/chat/composer/client.ts +17 -8
  43. package/src/shared/lib/chat/composer/commands.ts +290 -0
  44. package/src/shared/lib/chat/composer/composer.test.ts +157 -0
  45. package/src/shared/lib/chat/composer/filter.ts +21 -7
  46. package/src/shared/lib/chat/composer/trigger.ts +109 -20
  47. package/src/shared/lib/models/preferences.test.ts +131 -0
  48. package/src/shared/lib/models/preferences.ts +88 -0
  49. package/src/shared/lib/models/provider-models.ts +8 -1
  50. package/src/shared/lib/omp/subagent/label.test.ts +94 -0
  51. package/src/shared/lib/omp/subagent/label.ts +84 -0
  52. package/src/shared/lib/workspace/panel-widths.test.ts +113 -18
  53. package/src/shared/lib/workspace/panel-widths.ts +161 -30
  54. package/src/shared/lib/workspace/right-panels.ts +50 -11
  55. package/src/shared/lib/workspace/session-state/store.ts +3 -0
  56. package/src/shared/types/chat.ts +38 -65
  57. package/src/shared/types/index.ts +1 -0
  58. package/src/shared/types/models.ts +101 -0
  59. package/src/shared/types/omp/subagent.ts +4 -0
  60. package/src/shared/types/settings/command.ts +13 -0
  61. package/dist/client/static/css/index.e880517fcd.css +0 -2
  62. package/dist/client/static/js/index.0de15c5e17.js +0 -33
  63. /package/dist/client/static/js/async/{5b.0d6ae50fe8.js.LICENSE.txt → 5b.a5ccbc19dc.js.LICENSE.txt} +0 -0
  64. /package/dist/client/static/js/{index.0de15c5e17.js.LICENSE.txt → index.6e065017eb.js.LICENSE.txt} +0 -0
@@ -8,75 +8,192 @@
8
8
  * like a reset. The chat column is the group's filler — flexbox sizes it — so
9
9
  * it has no remembered width, only a floor it may not be pushed below.
10
10
  *
11
- * Sizes are pixels — the unit the panel constraints are written in — and are
12
- * persisted as `app_settings.desktopLayoutSizes`.
11
+ * Two units, deliberately:
12
+ *
13
+ * - The **sidebar** is pixels. It holds a session list, which does not get more
14
+ * useful on a wider monitor, and OpenChamber's sidebar is pixels too.
15
+ * - The **editor and right panels** are a *share of the group's available
16
+ * area*. A pixel width is only right on the display it was dragged on; a
17
+ * fraction follows a window resize. A remembered pixel value is kept
18
+ * alongside as the fallback for the window before the area is measured and
19
+ * for blobs written before the fraction existed.
20
+ *
21
+ * Sizes persist as `app_settings.desktopLayoutSizes`.
13
22
  */
14
23
  import { isRightPanelType, type RightPanelType } from '@/shared/lib/workspace/right-panels';
15
24
 
16
25
  /** The editor panel keeps a separate width while a diff tab is active. */
17
26
  export type EditorWidthMode = 'editor' | 'diff';
18
27
 
28
+ /**
29
+ * A panel width in both units. `fraction` is authoritative once the group's
30
+ * area is known; `px` is what the panel falls back to before then, and what a
31
+ * blob written before fractions existed carries.
32
+ */
33
+ export interface PanelWidth {
34
+ px?: number;
35
+ fraction?: number;
36
+ }
37
+
19
38
  export interface PanelWidths {
20
- /** Session sidebar. */
39
+ /** Session sidebar. Pixels only — it does not scale with the window. */
21
40
  left?: number;
22
41
  /** Editor panel while a source file tab is active. */
23
- editor?: number;
42
+ editor?: PanelWidth;
24
43
  /** Editor panel while a diff tab is active. */
25
- diff?: number;
44
+ diff?: PanelWidth;
26
45
  /** Right panel, keyed by the activity-bar view it currently shows. */
27
- right?: Partial<Record<RightPanelType, number>>;
46
+ right?: Partial<Record<RightPanelType, PanelWidth>>;
28
47
  }
29
48
 
30
- /** Fallbacks for panels that have never been resized. */
31
- export const DEFAULT_PANEL_WIDTHS: Record<EditorWidthMode | 'left', number> = {
32
- left: 268,
49
+ /**
50
+ * Fraction each editor mode opens at, and the px width used before the group's
51
+ * area is known. The diff fraction matches OpenChamber's `diff` surface (3/5);
52
+ * its px fallback stays wider than the source editor's because `SplitView`
53
+ * states a 700px floor of its own, so anything below it scrolls sideways.
54
+ */
55
+ export const DEFAULT_EDITOR_FRACTIONS: Record<EditorWidthMode, number> = {
56
+ editor: 0.6,
57
+ diff: 0.6,
58
+ };
59
+
60
+ export const DEFAULT_EDITOR_WIDTHS: Record<EditorWidthMode, number> = {
33
61
  editor: 600,
34
- // Side-by-side diffs need both halves readable at once: `SplitView` states a
35
- // 700px floor of its own, so anything below it scrolls sideways on open.
36
62
  diff: 720,
37
63
  };
38
64
 
65
+ /** Fallback for the sidebar, which has no fraction. */
66
+ export const DEFAULT_LEFT_PANEL_WIDTH = 280;
67
+
39
68
  /**
40
69
  * Panel floors and ceilings. The sidebar ceiling and the chat floor are a pair:
41
70
  * `MIN_CHAT_PANEL_WIDTH` is what stops the workspace stack from eating the
42
- * conversation, and it is also why the sidebar cannot open past 520 and still
43
- * leave the editor (300) and a browser view (320) room on a 1440px display.
71
+ * conversation, and it is also why the sidebar cannot open past 500 and still
72
+ * leave the editor (320) and a browser view (320) room on a 1440px display.
73
+ *
74
+ * `MIN_EDITOR_PANEL_WIDTH` matches the shared right-panel floor so the two
75
+ * panels cannot disagree about the narrowest width the same content is shown
76
+ * at.
44
77
  */
45
- export const MIN_LEFT_PANEL_WIDTH = 200;
46
- export const MAX_LEFT_PANEL_WIDTH = 520;
47
- export const MIN_CHAT_PANEL_WIDTH = 420;
48
- export const MIN_EDITOR_PANEL_WIDTH = 300;
49
- export const MAX_EDITOR_PANEL_WIDTH = 1200;
78
+ export const MIN_LEFT_PANEL_WIDTH = 264;
79
+ export const MAX_LEFT_PANEL_WIDTH = 500;
80
+ export const MIN_CHAT_PANEL_WIDTH = 400;
81
+ export const MIN_EDITOR_PANEL_WIDTH = 320;
82
+
83
+ /** Draggable separator width in px (`w-[3px]` in `ResizeHandle`). */
84
+ export const HANDLE_WIDTH = 3;
85
+
86
+ /** How much of a drag may re-apply the real width, in ms. */
87
+ export const RESIZE_FOLLOW_INTERVAL_MS = 100;
88
+
89
+ export interface ResolveWidthArgs {
90
+ /** Remembered width for this slot, if the user ever resized it. */
91
+ stored?: PanelWidth;
92
+ /** Fraction this slot opens at when nothing was remembered. */
93
+ defaultFraction: number;
94
+ /** px this slot opens at before the area is known. */
95
+ defaultPx: number;
96
+ /** Measured area of the group, or null before the observer reports. */
97
+ available: number | null;
98
+ min: number;
99
+ /** Floors the other fixed panels in the same group must keep. */
100
+ siblingMin: number;
101
+ /** Separators in the group, which also consume width. */
102
+ handleCount: number;
103
+ }
50
104
 
51
- function pixelWidth(value: unknown): number | undefined {
105
+ /**
106
+ * Resolve one panel's width in px.
107
+ *
108
+ * The ceiling is dynamic — `available − chat floor − the other panels' floors`
109
+ * — rather than a fixed number, because a fixed one either wastes a large
110
+ * monitor or eats the chat on a small one. Both the editor and the right panel
111
+ * compete for the same budget, so whichever renders first takes its share and
112
+ * the other gets what is left; the caller passes `siblingMin` to say which.
113
+ *
114
+ * Without a measured area there is no budget to divide, so the stored or
115
+ * default pixel width is used unchanged.
116
+ */
117
+ export function resolvePanelWidth(args: ResolveWidthArgs): number {
118
+ const { stored, defaultFraction, defaultPx, available, min, siblingMin, handleCount } = args;
119
+
120
+ if (available === null || available <= 0) return Math.max(min, Math.round(stored?.px ?? defaultPx));
121
+
122
+ const ceiling = Math.max(
123
+ min,
124
+ Math.floor(available - MIN_CHAT_PANEL_WIDTH - siblingMin - handleCount * HANDLE_WIDTH),
125
+ );
126
+
127
+ // A remembered fraction wins. A px-only value is honoured as written — the
128
+ // conversion below is an identity, deliberately: a pixel width someone chose
129
+ // is not silently rescaled, and it starts following the window only once that
130
+ // slot is dragged, which stores a fraction beside it. Reading rather than
131
+ // rewriting the stored blob is also what keeps a session's own widths from
132
+ // being clobbered by the global seed before that session's blob has loaded.
133
+ const fraction = stored?.fraction
134
+ ?? (stored?.px != null ? Math.min(1, stored.px / available) : defaultFraction);
135
+ const wanted = Math.round(fraction * available);
136
+
137
+ return Math.min(Math.max(wanted, min), ceiling);
138
+ }
139
+
140
+ function panelWidth(value: unknown): PanelWidth | undefined {
141
+ if (typeof value === 'number') {
142
+ return Number.isFinite(value) && value > 0 ? { px: Math.round(value) } : undefined;
143
+ }
144
+ if (!value || typeof value !== 'object') return undefined;
145
+
146
+ const source = value as Record<string, unknown>;
147
+ const px = Number.isFinite(source.px) && (source.px as number) > 0 ? Math.round(source.px as number) : undefined;
148
+ const fraction = Number.isFinite(source.fraction) && (source.fraction as number) > 0 && (source.fraction as number) <= 1
149
+ ? (source.fraction as number)
150
+ : undefined;
151
+
152
+ if (px === undefined && fraction === undefined) return undefined;
153
+ return { ...(px !== undefined ? { px } : {}), ...(fraction !== undefined ? { fraction } : {}) };
154
+ }
155
+
156
+ function sidebarWidth(value: unknown): number | undefined {
52
157
  return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : undefined;
53
158
  }
54
159
 
55
160
  /**
56
- * Coerce a stored blob into the current shape. Blobs written before per-view
57
- * widths held the right panel as a single number shared by every view; that
58
- * number belongs to whichever view was open when it was written, so it seeds
59
- * `legacyView` alone instead of being copied onto all eight.
161
+ * Coerce a stored blob into the current shape.
162
+ *
163
+ * Three older shapes are accepted, because a user's layout must survive the
164
+ * upgrade rather than silently reset:
165
+ *
166
+ * - a bare number is the px-only shape this app shipped before fractions;
167
+ * - a single number under `right` is older still, holding one width shared by
168
+ * every view — it belongs to whichever view was open when it was written, so
169
+ * it seeds `legacyView` alone instead of being copied onto all eight;
170
+ * - an unknown `right` key is dropped rather than trusted.
171
+ *
172
+ * Nothing is converted to a fraction here: this function is pure and the
173
+ * conversion needs the group's measured area. The layout performs it once the
174
+ * area is known (see `usePanelWidths`).
60
175
  */
61
176
  export function normalizePanelWidths(raw: unknown, legacyView: RightPanelType): PanelWidths {
62
177
  if (!raw || typeof raw !== 'object') return {};
63
178
  const source = raw as Record<string, unknown>;
64
179
 
65
180
  const widths: PanelWidths = {};
66
- for (const key of ['left', 'editor', 'diff'] as const) {
67
- const value = pixelWidth(source[key]);
181
+ const left = sidebarWidth(source.left);
182
+ if (left !== undefined) widths.left = left;
183
+ for (const key of ['editor', 'diff'] as const) {
184
+ const value = panelWidth(source[key]);
68
185
  if (value !== undefined) widths[key] = value;
69
186
  }
70
187
 
71
- const right: Partial<Record<RightPanelType, number>> = {};
188
+ const right: Partial<Record<RightPanelType, PanelWidth>> = {};
72
189
  if (source.right && typeof source.right === 'object') {
73
190
  for (const [view, value] of Object.entries(source.right as Record<string, unknown>)) {
74
191
  if (!isRightPanelType(view)) continue;
75
- const size = pixelWidth(value);
192
+ const size = panelWidth(value);
76
193
  if (size !== undefined) right[view] = size;
77
194
  }
78
195
  } else {
79
- const legacy = pixelWidth(source.right);
196
+ const legacy = panelWidth(source.right);
80
197
  if (legacy !== undefined) right[legacyView] = legacy;
81
198
  }
82
199
  if (Object.keys(right).length > 0) widths.right = right;
@@ -84,9 +201,23 @@ export function normalizePanelWidths(raw: unknown, legacyView: RightPanelType):
84
201
  return widths;
85
202
  }
86
203
 
87
- /** Fold a freshly measured patch over the stored widths (one level deep for `right`). */
204
+ /**
205
+ * Fold a freshly measured patch over the stored widths (one level deep for
206
+ * `right`, and within a slot for its two units — a fraction patch must not
207
+ * discard the px fallback it was derived from).
208
+ */
88
209
  export function mergePanelWidths(current: PanelWidths, patch: PanelWidths): PanelWidths {
89
210
  const next: PanelWidths = { ...current, ...patch };
90
- if (patch.right) next.right = { ...current.right, ...patch.right };
211
+ if (patch.right) {
212
+ const right: Partial<Record<RightPanelType, PanelWidth>> = { ...current.right };
213
+ for (const [view, value] of Object.entries(patch.right)) {
214
+ if (!isRightPanelType(view) || !value) continue;
215
+ right[view] = { ...current.right?.[view], ...value };
216
+ }
217
+ next.right = right;
218
+ }
219
+ for (const key of ['editor', 'diff'] as const) {
220
+ if (patch[key]) next[key] = { ...current[key], ...patch[key] };
221
+ }
91
222
  return next;
92
223
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Right developer panel catalog: the activity-bar views, the width each one
3
- * opens at, and the floor at which its content starts to clip.
2
+ * Right developer panel catalog: the activity-bar views, the share of the
3
+ * group's available area each one opens at, and the floor at which its content
4
+ * clips.
4
5
  *
5
6
  * The list lives here rather than in the activity bar component so the layout
6
7
  * can key one remembered width per view (`panel-widths.ts`) and validate a
@@ -21,36 +22,74 @@ export const RIGHT_PANEL_TYPES = [
21
22
 
22
23
  export type RightPanelType = (typeof RIGHT_PANEL_TYPES)[number];
23
24
 
24
- /** Width (px) a view opens at the first time it is shown. */
25
+ /**
26
+ * Share of the group's available area a view takes the first time it is shown.
27
+ *
28
+ * A fraction rather than a pixel width is what lets a panel follow a window
29
+ * resize instead of holding a number that was only right on the display it was
30
+ * dragged on. Where OpenChamber ships the same surface its `defaultWidthFraction`
31
+ * is reused verbatim; the rest are derived.
32
+ *
33
+ * - `files` is the file tree *alone*. OpenChamber's 3/5 `file` surface holds a
34
+ * tree **and** an editor column, so its fraction is not ours to borrow — their
35
+ * tree-only column is 200–480px, which is what this 0.22 approximates at a
36
+ * 1440-class viewport.
37
+ * - `search` matches `files`: both are single narrow columns of rows.
38
+ * - `usage` has no OpenChamber counterpart; 0.48 keeps the pixel default it
39
+ * shipped with.
40
+ * - `context`, `git`, `terminal` and both browsers take OpenChamber's values.
41
+ */
42
+ export const DEFAULT_RIGHT_PANEL_FRACTIONS: Record<RightPanelType, number> = {
43
+ context: 0.45,
44
+ files: 0.22,
45
+ search: 0.22,
46
+ git: 0.4,
47
+ // 80 columns at the terminal's 12px Fira Code (≈7.2px/char) need ≈600px.
48
+ terminal: 0.6,
49
+ 'user-browser': 0.45,
50
+ browser: 0.45,
51
+ usage: 0.48,
52
+ };
53
+
54
+ /**
55
+ * Width (px) a view opens at before the group's area has been measured. Kept at
56
+ * the values these views shipped with, so the first paint is unchanged and a
57
+ * window that never reports an area still opens sensibly.
58
+ */
25
59
  export const DEFAULT_RIGHT_PANEL_WIDTHS: Record<RightPanelType, number> = {
60
+ context: 536,
26
61
  files: 268,
27
62
  search: 268,
28
63
  git: 268,
29
- // 80 columns at the terminal's 12px Fira Code (≈7.2px/char) need ≈600px
30
- // before the scrollbar; the old 536 landed at ≈74 columns and wrapped.
31
64
  terminal: 640,
32
- context: 536,
33
65
  'user-browser': 804,
34
66
  browser: 804,
35
67
  usage: 536,
36
68
  };
37
69
 
38
- /** Minimum width (px) each view needs before its toolbar or list breaks. */
70
+ /**
71
+ * Minimum width (px) each view needs before its toolbar or list breaks.
72
+ *
73
+ * Per view, not one shared floor: a file tree reads fine at 200 while a shell
74
+ * does not, and OpenChamber draws the same line (its tree-only column floors at
75
+ * 200, its shared panel at 320). A uniform floor would either clip the tree or
76
+ * force every view to shell width.
77
+ */
39
78
  export const MIN_RIGHT_PANEL_WIDTHS: Record<RightPanelType, number> = {
40
79
  files: 200,
41
80
  search: 200,
81
+ // The commit graph is a fixed-width gutter plus a message column.
42
82
  git: 260,
43
- // A 200px terminal is ≈27 columns — below every shell's assumption; 320
44
- // holds a two-pane TUI or a git log with the graph.
83
+ // A 320px terminal is ≈27 columns — below every shell's assumption, but the
84
+ // point where a two-pane TUI or a git log with the graph still reads.
45
85
  terminal: 320,
86
+ // Telemetry charts and tables need room before they wrap.
46
87
  context: 420,
47
88
  'user-browser': 320,
48
89
  browser: 320,
49
90
  usage: 420,
50
91
  };
51
92
 
52
- export const MAX_RIGHT_PANEL_WIDTH = 1200;
53
-
54
93
  /** Guards a persisted or URL-provided view id before it keys a width. */
55
94
  export function isRightPanelType(value: unknown): value is RightPanelType {
56
95
  return typeof value === 'string' && (RIGHT_PANEL_TYPES as readonly string[]).includes(value);
@@ -12,6 +12,9 @@
12
12
  * Key namespace contract (dot-prefixed by owning panel):
13
13
  * - layout.activeRightPanel RightPanelType
14
14
  * - layout.showRightPanel boolean
15
+ * - layout.showLeftPanel boolean
16
+ * - layout.userToggledEditor boolean | null (null = follow whether a file is open)
17
+ * - layout.panelWidths PanelWidths (per-panel widths, seeded from app_settings.desktopLayoutSizes)
15
18
  * - layout.openedFiles editor file entries array
16
19
  * - layout.activeFileId number | null
17
20
  * - editor.previewMode Record<fileId, boolean>
@@ -187,71 +187,6 @@ export interface QueuedMessage {
187
187
  model: QueuedMessageModel | null;
188
188
  }
189
189
 
190
- export interface AIModelOption {
191
- id: string;
192
- name: string;
193
- provider: string;
194
- providerIcon?: string;
195
- contextWindow?: string | number;
196
- isCmdAgent?: boolean;
197
- isFavorite?: boolean;
198
- isRecent?: boolean;
199
- thinkingLevel?: string;
200
- /** Baked thinking ladder for this model: `["off", ...efforts]`. */
201
- thinkingLevels?: string[];
202
- capabilities?: string[];
203
- input?: string;
204
- output?: string;
205
- cost?: {
206
- input: string | number;
207
- output: string | number;
208
- cacheRead?: string | number;
209
- cacheWrite?: string | number;
210
- };
211
- description?: string;
212
- }
213
-
214
- // ── Real omp model registry (forked from omp-web) ──────────────────────────
215
-
216
- /** One pickable model from the omp catalog (`/api/models` modelList). */
217
- export interface ModelEntry {
218
- id: string;
219
- name: string;
220
- provider: string;
221
- supportsFastMode?: boolean;
222
- contextWindow?: number;
223
- maxTokens?: number;
224
- /** Baked thinking ladder for this model: `["off", ...efforts]`. */
225
- thinkingLevels?: string[];
226
- /** Cost in $/1M tokens (omp catalog). */
227
- cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
228
- }
229
-
230
- /** A model selected by provider + modelId (omp's set_model shape). */
231
- export interface SelectedModel {
232
- provider: string;
233
- modelId: string;
234
- }
235
-
236
- /** Model-level thinking metadata read off the live session state. */
237
- export interface ThinkingModelMeta {
238
- provider: string;
239
- modelId: string;
240
- name?: string;
241
- reasoning?: boolean;
242
- thinking?: { efforts?: string[] };
243
- }
244
-
245
- /** Response shape of GET /api/models (mirrors omp-web ModelsData). */
246
- export interface ModelsData {
247
- models: Record<string, string>;
248
- modelList: ModelEntry[];
249
- defaultModel: SelectedModel | null;
250
- thinkingLevels: Record<string, string[]>;
251
- connectedProviders?: { id: string; name: string; disabled: boolean }[];
252
- modelError?: string;
253
- }
254
-
255
190
  // ── Composer autocomplete (@file/agent / /command+skill) ───────────────────
256
191
 
257
192
  /** Trigger group that opened the composer autocomplete: `@` → mention, `/` → command. */
@@ -259,6 +194,13 @@ export type ComposerPickKind = 'mention' | 'command';
259
194
  /** Origin of a pickable item: an `@` mention is an agent or a workspace file. */
260
195
  export type ComposerPickSource = 'agent' | 'file' | 'command' | 'skill';
261
196
 
197
+ /**
198
+ * Which part of a slash invocation the popup is completing. oh-my-pi splits
199
+ * these the same way: `name` completes the command token itself, `args`
200
+ * completes a declarative subcommand after `<command> `.
201
+ */
202
+ export type ComposerTriggerPhase = 'name' | 'args';
203
+
262
204
  /** A detected trigger: the `@`/`/` char plus the query typed after it. */
263
205
  export interface ComposerTrigger {
264
206
  kind: ComposerPickKind;
@@ -267,6 +209,18 @@ export interface ComposerTrigger {
267
209
  start: number;
268
210
  /** Caret index when detected. */
269
211
  end: number;
212
+ /** Command completion phase; `mention` triggers are always `name`. */
213
+ phase: ComposerTriggerPhase;
214
+ /** Index the accepted text replaces from — equals `start` in the `name` phase. */
215
+ replaceFrom: number;
216
+ /** Command the `args` phase belongs to (name or alias as typed). */
217
+ command?: string;
218
+ /**
219
+ * The `/token` sits mid-prompt (prose precedes it on the line or an earlier
220
+ * line exists), so only skill entries may surface — oh-my-pi's
221
+ * `buildMidPromptSkillCompletions` gate.
222
+ */
223
+ midPrompt?: boolean;
270
224
  }
271
225
 
272
226
  /** One autocomplete item surfaced in the composer popover. */
@@ -281,6 +235,25 @@ export interface ComposerPickItem {
281
235
  token: string;
282
236
  /** Workspace-relative path (file items only). */
283
237
  path?: string;
238
+ /**
239
+ * Suppress the trailing space on insertion. Set on the collapsed `/skill:`
240
+ * namespace row so the popup reopens with the individual skills, matching
241
+ * oh-my-pi's accept path.
242
+ */
243
+ insertWithoutSpace?: boolean;
244
+ /** Command aliases from oh-my-pi (`/models` for `/model`). */
245
+ aliases?: string[];
246
+ /** Declarative subcommands; drives the `args` phase. */
247
+ subcommands?: ComposerSubcommand[];
248
+ /** Argument hint (`[on|off|status]`), prefixed to the description like omp. */
249
+ inputHint?: string;
250
+ }
251
+
252
+ /** One declarative subcommand of an oh-my-pi slash command. */
253
+ export interface ComposerSubcommand {
254
+ name: string;
255
+ description?: string;
256
+ usage?: string;
284
257
  }
285
258
 
286
259
  /** A pick item after filtering, with the matched range within `name` (or null). */
@@ -2,6 +2,7 @@ export * from '@/shared/types/workspace';
2
2
  export * from '@/shared/types/fs';
3
3
  export * from '@/shared/types/git';
4
4
  export * from '@/shared/types/chat';
5
+ export * from '@/shared/types/models';
5
6
  export * from '@/shared/types/terminal';
6
7
  export * from '@/shared/types/settings/state';
7
8
  export * from '@/shared/types/tokenUsage';
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Model catalog types shared by the composer picker, the settings pages and
8
+ * the `/api/models` route: the picker row (`AIModelOption`), the registry entry
9
+ * (`ModelEntry`), the selection reference omp's `set_model` takes, and the
10
+ * stored favorite/recent preference pair.
11
+ *
12
+ * Split out of `chat.ts` so that file stays under the per-file ceiling — the
13
+ * picker's row shape is not part of the chat message model it was nested in.
14
+ */
15
+
16
+ export interface AIModelOption {
17
+ id: string;
18
+ name: string;
19
+ provider: string;
20
+ providerIcon?: string;
21
+ contextWindow?: string | number;
22
+ isCmdAgent?: boolean;
23
+ /**
24
+ * Favorite / last-used flag. On a picker row these are DERIVED from the
25
+ * stored preference keys (`ModelPreferences`), never read off the registry —
26
+ * a favorite is a chamber-local preference, not a property of omp's config.
27
+ * On the MOCK catalog they are the seed for a fresh install's rails.
28
+ */
29
+ isFavorite?: boolean;
30
+ /** MOCK seed only — the RECENT rail is ordered by `ModelPreferences.recentKeys`. */
31
+ isRecent?: boolean;
32
+ thinkingLevel?: string;
33
+ /** Baked thinking ladder for this model: `["off", ...efforts]`. */
34
+ thinkingLevels?: string[];
35
+ capabilities?: string[];
36
+ input?: string;
37
+ output?: string;
38
+ cost?: {
39
+ input: string | number;
40
+ output: string | number;
41
+ cacheRead?: string | number;
42
+ cacheWrite?: string | number;
43
+ };
44
+ description?: string;
45
+ }
46
+
47
+ // ── Real omp model registry (forked from omp-web) ──────────────────────────
48
+
49
+ /** One pickable model from the omp catalog (`/api/models` modelList). */
50
+ export interface ModelEntry {
51
+ id: string;
52
+ name: string;
53
+ provider: string;
54
+ supportsFastMode?: boolean;
55
+ contextWindow?: number;
56
+ maxTokens?: number;
57
+ /** Baked thinking ladder for this model: `["off", ...efforts]`. */
58
+ thinkingLevels?: string[];
59
+ /** Cost in $/1M tokens (omp catalog). */
60
+ cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
61
+ }
62
+
63
+ /** A model selected by provider + modelId (omp's set_model shape). */
64
+ export interface SelectedModel {
65
+ provider: string;
66
+ modelId: string;
67
+ }
68
+
69
+ /** Model-level thinking metadata read off the live session state. */
70
+ export interface ThinkingModelMeta {
71
+ provider: string;
72
+ modelId: string;
73
+ name?: string;
74
+ reasoning?: boolean;
75
+ thinking?: { efforts?: string[] };
76
+ }
77
+
78
+ /**
79
+ * The user's favorite + last-used models, keyed by the composite `provider:id`
80
+ * identity (`modelKey`). Stored separately from the model registry because the
81
+ * registry is a read-only view of omp's config — a favorite is a chamber-local
82
+ * preference, and an id-only key would light up every provider serving it.
83
+ */
84
+ export interface ModelPreferences {
85
+ /** Composite keys in the order the FAVORITES rail renders them. */
86
+ favorites: string[];
87
+ /** Composite keys, most recently used first, capped at `RECENT_MODELS_LIMIT`. */
88
+ recentKeys: string[];
89
+ }
90
+
91
+ /** Response shape of GET /api/models (mirrors omp-web ModelsData). */
92
+ export interface ModelsData {
93
+ models: Record<string, string>;
94
+ modelList: ModelEntry[];
95
+ defaultModel: SelectedModel | null;
96
+ thinkingLevels: Record<string, string[]>;
97
+ connectedProviders?: { id: string; name: string; disabled: boolean }[];
98
+ modelError?: string;
99
+ /** Absent when the settings row has never been written. */
100
+ modelPreferences?: ModelPreferences;
101
+ }
@@ -150,6 +150,10 @@ export interface SubagentInfo {
150
150
  detached?: boolean;
151
151
  progress?: SubagentProgress;
152
152
  lastUpdate?: number;
153
+ /** Model role the run resolved to. On-disk history entries carry it here
154
+ * (folded from the parent's task toolResult); live snapshots carry it on
155
+ * `progress` instead, because it arrives with the progress frame. */
156
+ modelRole?: string;
153
157
  /** Settled result for history entries (SingleResult-derived). */
154
158
  result?: SubagentHistoryResult;
155
159
  /** Roster origin: live frames/snapshots (default) vs on-disk history. */
@@ -1,5 +1,12 @@
1
1
  export type CommandScope = 'system' | 'user' | 'project';
2
2
 
3
+ /** Declarative subcommand of an oh-my-pi builtin (drives `/cmd <sub>` completion). */
4
+ export interface CommandSubcommand {
5
+ name: string;
6
+ description?: string;
7
+ usage?: string;
8
+ }
9
+
3
10
  export interface CommandItem {
4
11
  id: string;
5
12
  name: string;
@@ -9,4 +16,10 @@ export interface CommandItem {
9
16
  overrideModel?: string;
10
17
  template: string;
11
18
  isBuiltIn?: boolean;
19
+ /** oh-my-pi aliases (`/models` for `/model`); live agent commands only. */
20
+ aliases?: string[];
21
+ /** Declarative subcommands; live agent commands only. */
22
+ subcommands?: CommandSubcommand[];
23
+ /** Argument hint (`[on|off|status]`); live agent commands only. */
24
+ inputHint?: string;
12
25
  }