@ericsanchezok/synergy-workbench 0.0.0-stage → 3.1.1

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 (153) hide show
  1. package/AGENTS.md +30 -0
  2. package/README.md +16 -2
  3. package/dist/agents.js +11 -0
  4. package/dist/cli-adapter.js +8 -0
  5. package/dist/component.js +65 -0
  6. package/dist/config-schema.js +265 -0
  7. package/dist/execution/activity.js +78 -0
  8. package/dist/execution/content.js +184 -0
  9. package/dist/execution/json-sections.js +231 -0
  10. package/dist/execution/presentation.js +79 -0
  11. package/dist/execution/preview.js +13 -0
  12. package/dist/execution/process.js +125 -0
  13. package/dist/execution/projection.js +327 -0
  14. package/dist/execution/query.js +18 -0
  15. package/dist/execution/routes.js +189 -0
  16. package/dist/execution/schema.js +322 -0
  17. package/dist/execution/service.js +1993 -0
  18. package/dist/features.js +22 -0
  19. package/dist/http.js +65 -0
  20. package/dist/performance/analysis.js +330 -0
  21. package/dist/performance/catalog.js +391 -0
  22. package/dist/performance/dashboard.js +483 -0
  23. package/dist/performance/error.js +15 -0
  24. package/dist/performance/index.js +6 -0
  25. package/dist/performance/inflight.js +43 -0
  26. package/dist/performance/projection.js +138 -0
  27. package/dist/performance/prompt/base.txt +38 -0
  28. package/dist/performance/prompt/builder.js +21 -0
  29. package/dist/performance/routes/performance-route.js +439 -0
  30. package/dist/performance/schema.js +628 -0
  31. package/dist/performance/timeline.js +98 -0
  32. package/dist/performance/trace-detail.js +91 -0
  33. package/dist/presentation/service.js +270 -0
  34. package/dist/presentation/tools/workbench-view.js +65 -0
  35. package/dist/presentation/tools.js +10 -0
  36. package/dist/project/directories.js +363 -0
  37. package/dist/project/git-health.js +498 -0
  38. package/dist/project/ide.js +66 -0
  39. package/dist/project/migration.js +22 -0
  40. package/dist/project/routes/directories.js +89 -0
  41. package/dist/project/routes/git.js +30 -0
  42. package/dist/project/routes/task-defaults.js +42 -0
  43. package/dist/project/session-health.js +9 -0
  44. package/dist/project/startup.js +28 -0
  45. package/dist/project/task-defaults.js +68 -0
  46. package/dist/project/tools/session-control.js +664 -0
  47. package/dist/project/tools/session-control.txt +60 -0
  48. package/dist/project/tools/worktree-archive.js +76 -0
  49. package/dist/project/tools/worktree-enter.js +175 -0
  50. package/dist/project/tools/worktree-leave.js +119 -0
  51. package/dist/project/tools/worktree-list.js +52 -0
  52. package/dist/project/tools.js +26 -0
  53. package/dist/project/vcs-branch-watcher.js +76 -0
  54. package/dist/project/vcs.js +114 -0
  55. package/dist/project/worktree-janitor.js +140 -0
  56. package/dist/project/worktrees.js +98 -0
  57. package/dist/push/bridge.js +171 -0
  58. package/dist/push/index.js +4 -0
  59. package/dist/push/routes/push.js +224 -0
  60. package/dist/push/service.js +93 -0
  61. package/dist/push/store.js +121 -0
  62. package/dist/push/types.js +58 -0
  63. package/dist/review/git.js +226 -0
  64. package/dist/review/routes.js +45 -0
  65. package/dist/review/schema.js +32 -0
  66. package/dist/review/state.js +79 -0
  67. package/dist/stats/aggregator.js +252 -0
  68. package/dist/stats/canonical.js +89 -0
  69. package/dist/stats/cli/stats.js +302 -0
  70. package/dist/stats/engine.js +176 -0
  71. package/dist/stats/index.js +5 -0
  72. package/dist/stats/rollup.js +460 -0
  73. package/dist/stats/routes/stats.js +94 -0
  74. package/dist/stats/routes/usage.js +83 -0
  75. package/dist/stats/storage.js +86 -0
  76. package/dist/stats/types.js +267 -0
  77. package/dist/worker.js +6 -0
  78. package/package.json +232 -4
  79. package/src/agents.ts +15 -0
  80. package/src/cli-adapter.ts +11 -0
  81. package/src/component.ts +73 -0
  82. package/src/config-schema.ts +289 -0
  83. package/src/execution/activity.ts +83 -0
  84. package/src/execution/content.ts +189 -0
  85. package/src/execution/json-sections.ts +230 -0
  86. package/src/execution/presentation.ts +91 -0
  87. package/src/execution/preview.ts +12 -0
  88. package/src/execution/process.ts +118 -0
  89. package/src/execution/projection.ts +330 -0
  90. package/src/execution/query.ts +19 -0
  91. package/src/execution/routes.ts +324 -0
  92. package/src/execution/schema.ts +336 -0
  93. package/src/execution/service.ts +2149 -0
  94. package/src/features.ts +22 -0
  95. package/src/http.ts +75 -0
  96. package/src/performance/analysis.ts +356 -0
  97. package/src/performance/catalog.ts +902 -0
  98. package/src/performance/dashboard.ts +521 -0
  99. package/src/performance/error.ts +32 -0
  100. package/src/performance/index.ts +6 -0
  101. package/src/performance/inflight.ts +46 -0
  102. package/src/performance/projection.ts +143 -0
  103. package/src/performance/prompt/base.txt +38 -0
  104. package/src/performance/prompt/builder.ts +24 -0
  105. package/src/performance/routes/performance-route.ts +541 -0
  106. package/src/performance/schema.ts +695 -0
  107. package/src/performance/timeline.ts +100 -0
  108. package/src/performance/trace-detail.ts +97 -0
  109. package/src/presentation/service.ts +303 -0
  110. package/src/presentation/tools/workbench-view.ts +75 -0
  111. package/src/presentation/tools.ts +11 -0
  112. package/src/project/directories.ts +391 -0
  113. package/src/project/git-health.ts +577 -0
  114. package/src/project/ide.ts +80 -0
  115. package/src/project/migration.ts +24 -0
  116. package/src/project/routes/directories.ts +124 -0
  117. package/src/project/routes/git.ts +42 -0
  118. package/src/project/routes/task-defaults.ts +50 -0
  119. package/src/project/session-health.ts +10 -0
  120. package/src/project/startup.ts +28 -0
  121. package/src/project/task-defaults.ts +70 -0
  122. package/src/project/tools/session-control.ts +725 -0
  123. package/src/project/tools/session-control.txt +60 -0
  124. package/src/project/tools/worktree-archive.ts +92 -0
  125. package/src/project/tools/worktree-enter.ts +227 -0
  126. package/src/project/tools/worktree-leave.ts +140 -0
  127. package/src/project/tools/worktree-list.ts +68 -0
  128. package/src/project/tools.ts +29 -0
  129. package/src/project/vcs-branch-watcher.ts +81 -0
  130. package/src/project/vcs.ts +124 -0
  131. package/src/project/worktree-janitor.ts +153 -0
  132. package/src/project/worktrees.ts +110 -0
  133. package/src/push/bridge.ts +182 -0
  134. package/src/push/index.ts +4 -0
  135. package/src/push/routes/push.ts +267 -0
  136. package/src/push/service.ts +108 -0
  137. package/src/push/store.ts +120 -0
  138. package/src/push/types.ts +67 -0
  139. package/src/review/git.ts +221 -0
  140. package/src/review/routes.ts +65 -0
  141. package/src/review/schema.ts +34 -0
  142. package/src/review/state.ts +77 -0
  143. package/src/stats/aggregator.ts +277 -0
  144. package/src/stats/canonical.ts +93 -0
  145. package/src/stats/cli/stats.ts +348 -0
  146. package/src/stats/engine.ts +197 -0
  147. package/src/stats/index.ts +5 -0
  148. package/src/stats/rollup.ts +522 -0
  149. package/src/stats/routes/stats.ts +107 -0
  150. package/src/stats/routes/usage.ts +103 -0
  151. package/src/stats/storage.ts +78 -0
  152. package/src/stats/types.ts +351 -0
  153. package/src/worker.ts +7 -0
@@ -0,0 +1,73 @@
1
+ import { componentFeatures } from "./features"
2
+ import { BusEvent } from "@ericsanchezok/synergy-harness/bus/bus-event"
3
+ import { Vcs } from "./project/vcs"
4
+ import { Ide } from "./project/ide"
5
+ import { ReviewState } from "./review/state"
6
+ import { ExecutionSchema } from "./execution/schema"
7
+ import { registerProjectMigrations } from "./project/migration"
8
+ import { PushBridge } from "./push/bridge"
9
+ import type { RuntimeComponent } from "@ericsanchezok/synergy-harness/lifecycle"
10
+ import { version } from "../package.json" with { type: "json" }
11
+ import { registerWorkbenchAgents } from "./agents"
12
+ import { registerProjectTools } from "./project/tools"
13
+ import { registerProjectStartup } from "./project/startup"
14
+ import { registerProjectSessionHealth } from "./project/session-health"
15
+ import { registerConfig } from "./config-schema"
16
+ import { Usage } from "@ericsanchezok/synergy-harness/usage"
17
+ import { ExecutionService } from "./execution/service"
18
+ import { WorkbenchPresentation } from "./presentation/service"
19
+ import { registerPresentationTools } from "./presentation/tools"
20
+
21
+ export function workbench(): RuntimeComponent {
22
+ return {
23
+ id: "workbench",
24
+ features: componentFeatures,
25
+ apiVersion: 1,
26
+ version,
27
+ requires: { "local-runtime": version },
28
+ workers: { agent: new URL("./worker.ts", import.meta.url) },
29
+ services() {
30
+ let dispose: (() => void) | undefined
31
+ let stopUsage: (() => Promise<void>) | undefined
32
+ let stopExecution: (() => void) | undefined
33
+ let stopPresentation: (() => void) | undefined
34
+ return {
35
+ resident: {
36
+ async start() {
37
+ dispose = PushBridge.init()
38
+ stopUsage = Usage.service()
39
+ stopExecution = ExecutionService.init()
40
+ stopPresentation = WorkbenchPresentation.init()
41
+ },
42
+ async stop() {
43
+ stopPresentation?.()
44
+ stopPresentation = undefined
45
+ await stopUsage?.()
46
+ stopExecution?.()
47
+ stopUsage = undefined
48
+ dispose?.()
49
+ dispose = undefined
50
+ await PushBridge.flush()
51
+ },
52
+ },
53
+ }
54
+ },
55
+ adapters: { cli: new URL("./cli-adapter.ts", import.meta.url), http: new URL("./http.ts", import.meta.url) },
56
+ register() {
57
+ BusEvent.register(
58
+ ...Object.values(Vcs.Event),
59
+ ...Object.values(Ide.Event),
60
+ ReviewState.Updated,
61
+ ExecutionSchema.Updated,
62
+ ExecutionSchema.ProjectionUpdated,
63
+ )
64
+ registerConfig()
65
+ registerProjectMigrations()
66
+ registerWorkbenchAgents()
67
+ registerProjectTools()
68
+ registerPresentationTools()
69
+ registerProjectStartup()
70
+ registerProjectSessionHealth()
71
+ },
72
+ }
73
+ }
@@ -0,0 +1,289 @@
1
+ import { ConfigDomain } from "@ericsanchezok/synergy-harness/config/domain"
2
+ import z from "zod"
3
+ import { ConfigExtensions } from "@ericsanchezok/synergy-harness/config/extensions"
4
+ export const Keybinds = z
5
+ .object({
6
+ leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
7
+ app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"),
8
+ editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
9
+ theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
10
+ sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
11
+ scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"),
12
+ username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
13
+ status_view: z.string().optional().default("<leader>s").describe("View status"),
14
+ session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
15
+ session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
16
+ session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
17
+ session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
18
+ session_fork: z.string().optional().default("none").describe("Fork session from message"),
19
+ session_rename: z.string().optional().default("none").describe("Rename session"),
20
+ session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
21
+ session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
22
+ messages_page_up: z.string().optional().default("pageup").describe("Scroll messages up by one page"),
23
+ messages_page_down: z.string().optional().default("pagedown").describe("Scroll messages down by one page"),
24
+ messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
25
+ messages_half_page_down: z.string().optional().default("ctrl+alt+d").describe("Scroll messages down by half page"),
26
+ messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
27
+ messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
28
+ messages_next: z.string().optional().default("none").describe("Navigate to next message"),
29
+ messages_previous: z.string().optional().default("none").describe("Navigate to previous message"),
30
+ messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
31
+ messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
32
+ messages_undo: z.string().optional().default("<leader>u").describe("Undo message history only"),
33
+ messages_redo: z.string().optional().default("<leader>r").describe("Redo message history only"),
34
+ messages_toggle_conceal: z
35
+ .string()
36
+ .optional()
37
+ .default("<leader>h")
38
+ .describe("Toggle code block concealment in messages"),
39
+ tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"),
40
+ model_list: z.string().optional().default("<leader>m").describe("List available models"),
41
+ model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
42
+ model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
43
+ model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"),
44
+ model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
45
+ command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
46
+ agent_list: z.string().optional().default("<leader>a").describe("List agents"),
47
+ agent_cycle: z.string().optional().default("tab").describe("Next agent"),
48
+ agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
49
+ variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
50
+ input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
51
+ input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
52
+ input_submit: z.string().optional().default("return").describe("Submit input"),
53
+ input_newline: z
54
+ .string()
55
+ .optional()
56
+ .default("shift+return,ctrl+return,alt+return,ctrl+j")
57
+ .describe("Insert newline in input"),
58
+ input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"),
59
+ input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"),
60
+ input_move_up: z.string().optional().default("up").describe("Move cursor up in input"),
61
+ input_move_down: z.string().optional().default("down").describe("Move cursor down in input"),
62
+ input_select_left: z.string().optional().default("shift+left").describe("Select left in input"),
63
+ input_select_right: z.string().optional().default("shift+right").describe("Select right in input"),
64
+ input_select_up: z.string().optional().default("shift+up").describe("Select up in input"),
65
+ input_select_down: z.string().optional().default("shift+down").describe("Select down in input"),
66
+ input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"),
67
+ input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"),
68
+ input_select_line_home: z.string().optional().default("ctrl+shift+a").describe("Select to start of line in input"),
69
+ input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"),
70
+ input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"),
71
+ input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"),
72
+ input_select_visual_line_home: z
73
+ .string()
74
+ .optional()
75
+ .default("alt+shift+a")
76
+ .describe("Select to start of visual line in input"),
77
+ input_select_visual_line_end: z
78
+ .string()
79
+ .optional()
80
+ .default("alt+shift+e")
81
+ .describe("Select to end of visual line in input"),
82
+ input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"),
83
+ input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"),
84
+ input_select_buffer_home: z
85
+ .string()
86
+ .optional()
87
+ .default("shift+home")
88
+ .describe("Select to start of buffer in input"),
89
+ input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"),
90
+ input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"),
91
+ input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"),
92
+ input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
93
+ input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
94
+ input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
95
+ input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
96
+ input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
97
+ input_word_forward: z
98
+ .string()
99
+ .optional()
100
+ .default("alt+f,alt+right,ctrl+right")
101
+ .describe("Move word forward in input"),
102
+ input_word_backward: z
103
+ .string()
104
+ .optional()
105
+ .default("alt+b,alt+left,ctrl+left")
106
+ .describe("Move word backward in input"),
107
+ input_select_word_forward: z
108
+ .string()
109
+ .optional()
110
+ .default("alt+shift+f,alt+shift+right")
111
+ .describe("Select word forward in input"),
112
+ input_select_word_backward: z
113
+ .string()
114
+ .optional()
115
+ .default("alt+shift+b,alt+shift+left")
116
+ .describe("Select word backward in input"),
117
+ input_delete_word_forward: z
118
+ .string()
119
+ .optional()
120
+ .default("alt+d,alt+delete,ctrl+delete")
121
+ .describe("Delete word forward in input"),
122
+ input_delete_word_backward: z
123
+ .string()
124
+ .optional()
125
+ .default("ctrl+w,ctrl+backspace,alt+backspace")
126
+ .describe("Delete word backward in input"),
127
+ history_previous: z.string().optional().default("up").describe("Previous history item"),
128
+ history_next: z.string().optional().default("down").describe("Next history item"),
129
+ session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),
130
+ session_child_cycle_reverse: z.string().optional().default("<leader>left").describe("Previous child session"),
131
+ session_parent: z.string().optional().default("<leader>up").describe("Go to parent session"),
132
+ terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
133
+ terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
134
+ tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
135
+ })
136
+ .strict()
137
+ .meta({
138
+ ref: "KeybindsConfig",
139
+ })
140
+
141
+ export const Layout = z.enum(["auto", "stretch"]).meta({
142
+ ref: "LayoutConfig",
143
+ })
144
+
145
+ export type Layout = z.infer<typeof Layout>
146
+
147
+ const QuickSwitcherModel = z
148
+ .object({
149
+ providerID: z.string().describe("Provider id for the quick switcher model preference"),
150
+ modelID: z.string().describe("Model id for the quick switcher model preference"),
151
+ state: z.enum(["add", "remove"]).describe("Whether to force-add or force-remove the model from the quick switcher"),
152
+ })
153
+ .strict()
154
+ .meta({ ref: "QuickSwitcherModelConfig" })
155
+
156
+ export type QuickSwitcherModel = z.infer<typeof QuickSwitcherModel>
157
+
158
+ export const QuickSwitcher = z
159
+ .object({
160
+ models: z.array(QuickSwitcherModel).optional().describe("Per-model quick switcher visibility preferences"),
161
+ })
162
+ .strict()
163
+ .meta({ ref: "QuickSwitcherConfig" })
164
+
165
+ export type QuickSwitcher = z.infer<typeof QuickSwitcher>
166
+
167
+ export const ConfigShape = {
168
+ locale: z.enum(["system", "en", "zh-CN"]).optional().describe("UI locale (system = follow OS, default: system)"),
169
+ theme: z.string().optional().describe("Theme name to use for the interface"),
170
+ activityDisplay: z
171
+ .enum(["full", "balanced", "minimal"])
172
+ .optional()
173
+ .describe(
174
+ "Execution process detail: full = expanded process, balanced = current stage with completed process collapsed, minimal = compact progress. All modes use the same process view (default: balanced)",
175
+ ),
176
+ defaultSessionWorkspace: z
177
+ .enum(["main", "worktree"])
178
+ .optional()
179
+ .describe(
180
+ "Default workspace for new sessions started from the Web composer: main = run in the main checkout, " +
181
+ "worktree = start each new session in an isolated git worktree (default: main). " +
182
+ "Programmatic session creation (API, channels, Cortex) always uses the main checkout.",
183
+ ),
184
+ defaultSessionEnvironmentProfile: z
185
+ .string()
186
+ .min(1)
187
+ .nullable()
188
+ .optional()
189
+ .describe(
190
+ "Execution profile for new Web/Desktop composer sessions. Omitted follows the global resource default; null disables execution selection. References an existing global profile without defining hosts or credentials.",
191
+ ),
192
+ keybinds: Keybinds.optional().describe("Custom keybind configurations"),
193
+ compactReasoning: z
194
+ .boolean()
195
+ .optional()
196
+ .describe("Show a compact reasoning preview in the execution process (default: false)"),
197
+ preferWorkBrief: z
198
+ .boolean()
199
+ .optional()
200
+ .describe("Prefer a tool call's purpose description over its action and target when available (default: false)"),
201
+ quick_switcher: QuickSwitcher.optional().describe("Quick switcher model visibility preferences"),
202
+ welcomeGames: z
203
+ .boolean()
204
+ .optional()
205
+ .describe("Show interactive games on the new-task greeting (default: true). Set false to render a plain greeting."),
206
+ layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
207
+ toast: z
208
+ .object({
209
+ muted: z
210
+ .array(z.enum(["info", "success", "warning", "error"]))
211
+ .optional()
212
+ .describe("Toast types to suppress. The underlying logic still runs but the visual card is not rendered."),
213
+ durationOverrides: z
214
+ .record(z.enum(["info", "success", "warning", "error"]), z.number().int().positive().max(30000))
215
+ .optional()
216
+ .describe("Override auto-dismiss duration in ms per toast type (max 30s)."),
217
+ })
218
+ .strict()
219
+ .optional()
220
+ .describe("Toast notification preferences"),
221
+ }
222
+
223
+ export type ConfigValues = z.output<z.ZodObject<typeof ConfigShape>>
224
+
225
+ declare module "@ericsanchezok/synergy-harness/config/schema" {
226
+ interface ConfigExtensionShape extends ConfigShapeType {}
227
+ }
228
+ type ConfigShapeType = typeof ConfigShape
229
+
230
+ const contribution: ConfigExtensions.Contribution = {
231
+ shape: ConfigShape,
232
+ references(raw, providerID) {
233
+ const config = raw as ConfigValues
234
+ return (config.quick_switcher?.models ?? []).flatMap((model, index) =>
235
+ model.providerID === providerID ? [`quick_switcher.models[${index}]`] : [],
236
+ )
237
+ },
238
+ normalize(raw) {
239
+ const result = raw as ConfigValues
240
+ if (result.activityDisplay === undefined) result.activityDisplay = "balanced"
241
+ if (result.compactReasoning === undefined) result.compactReasoning = false
242
+ if (result.preferWorkBrief === undefined) result.preferWorkBrief = false
243
+ if (!result.keybinds) result.keybinds = Keybinds.parse({})
244
+ },
245
+ }
246
+
247
+ export function registerConfig() {
248
+ ConfigExtensions.register("workbench", contribution)
249
+ for (const domain of [
250
+ {
251
+ id: "general",
252
+ filename: "00-general.jsonc",
253
+ label: "General",
254
+ ownedKeys: [
255
+ "theme",
256
+ "keybinds",
257
+ "toast",
258
+ "compactReasoning",
259
+ "preferWorkBrief",
260
+ "welcomeGames",
261
+ "locale",
262
+ "activityDisplay",
263
+ "defaultSessionWorkspace",
264
+ "defaultSessionEnvironmentProfile",
265
+ "layout",
266
+ ],
267
+ mergePolicy: "merge",
268
+ reloadTargets: ["config"],
269
+ uiSection: "general",
270
+ importable: true,
271
+ },
272
+ {
273
+ id: "models",
274
+ filename: "10-models.jsonc",
275
+ label: "Models",
276
+ ownedKeys: ["quick_switcher"],
277
+ mergePolicy: "merge",
278
+ reloadTargets: ["config"],
279
+ uiSection: "models",
280
+ importable: true,
281
+ },
282
+ ] satisfies ConfigDomain.Definition[])
283
+ ConfigDomain.register(domain)
284
+ }
285
+
286
+ export async function readConfig(): Promise<ConfigValues> {
287
+ const { Config } = await import("@ericsanchezok/synergy-harness/config/config")
288
+ return Config.current()
289
+ }
@@ -0,0 +1,83 @@
1
+ import type { ExecutionSchema } from "./schema"
2
+
3
+ export namespace ExecutionActivity {
4
+ export type Segment = { runID: string | null; from: number; to: number; count: number; rounds: number }
5
+ export function project(rows: readonly ExecutionSchema.Node[], root: string) {
6
+ const byID = new Map(rows.map((node) => [node.id, node]))
7
+ const requests = new Set(rows.filter((node) => node.evidenceKind === "attempt").map((node) => node.parentID))
8
+ const instructions = new Set(
9
+ rows.filter((node) => node.kind === "subtask").map((node) => node.sessionID + ":" + node.runID),
10
+ )
11
+ const nodes = rows
12
+ .filter(
13
+ (node) =>
14
+ node.kind === "subtask" ||
15
+ node.kind === "tool" ||
16
+ (node.kind === "input" &&
17
+ (node.sessionID === root || !instructions.has(node.sessionID + ":" + node.runID))) ||
18
+ node.evidenceKind === "attempt" ||
19
+ (node.evidenceKind === "call" && !requests.has(node.id)),
20
+ )
21
+ .map((node, index) => ({
22
+ ...node,
23
+ activity: {
24
+ index,
25
+ count: 1,
26
+ endIndex: index,
27
+ instruction: node.kind === "subtask" || (node.kind === "input" && node.sessionID !== root),
28
+ },
29
+ }))
30
+ const segments: Segment[] = []
31
+ for (const node of nodes) {
32
+ let ancestor: ExecutionSchema.Node | undefined = node
33
+ const seen = new Set<string>()
34
+ while (ancestor && ancestor.sessionID !== root && ancestor.parentID && !seen.has(ancestor.id)) {
35
+ seen.add(ancestor.id)
36
+ ancestor = byID.get(ancestor.parentID)
37
+ }
38
+ const runID = ancestor?.sessionID === root ? ancestor.runID : null
39
+ const last = segments.at(-1)
40
+ if (last?.runID === runID) {
41
+ last.to = node.activity.index
42
+ last.count++
43
+ } else segments.push({ runID, from: node.activity.index, to: node.activity.index, count: 1, rounds: 1 })
44
+ }
45
+ const chunks: Segment[] = []
46
+ const size = Math.max(1, Math.ceil(segments.length / 160))
47
+ for (let index = 0; index < segments.length; index += size) {
48
+ const values = segments.slice(index, index + size)
49
+ chunks.push({
50
+ runID: values.length === 1 ? values[0].runID : null,
51
+ from: values[0].from,
52
+ to: values.at(-1)!.to,
53
+ count: values.reduce((sum, value) => sum + value.count, 0),
54
+ rounds: new Set(values.map((value) => value.runID)).size,
55
+ })
56
+ }
57
+ return {
58
+ nodes,
59
+ segments: chunks,
60
+ humanInputs: nodes.filter((node) => node.kind === "input" && !node.activity.instruction).length,
61
+ taskInstructions: nodes.filter((node) => node.activity.instruction).length,
62
+ }
63
+ }
64
+ export function aggregate(nodes: ReturnType<typeof project>["nodes"], kind: "input" | "model" | "tool") {
65
+ const lane = nodes.filter((node) =>
66
+ kind === "input"
67
+ ? node.kind === "input" || node.kind === "subtask"
68
+ : kind === "model"
69
+ ? node.evidenceKind === "call" || node.evidenceKind === "attempt"
70
+ : node.kind === "tool",
71
+ )
72
+ const size = Math.max(1, Math.ceil(lane.length / 160))
73
+ const result = []
74
+ for (let index = 0; index < lane.length; index += size) {
75
+ const bucket = lane.slice(index, index + size)
76
+ result.push({
77
+ ...bucket[0],
78
+ activity: { ...bucket[0].activity, count: bucket.length, endIndex: bucket.at(-1)!.activity.index },
79
+ })
80
+ }
81
+ return result
82
+ }
83
+ }
@@ -0,0 +1,189 @@
1
+ import { ExecutionQueryError, queryCursor } from "./query"
2
+ import { z } from "zod"
3
+ import { RuntimeContext } from "@ericsanchezok/synergy-harness/lifecycle/context"
4
+ import { RolloutEvidence } from "@ericsanchezok/synergy-harness/rollout"
5
+ import { ExecutionJson } from "./json-sections"
6
+ import { ScopeContext } from "@ericsanchezok/synergy-harness/scope/context"
7
+
8
+ export namespace ExecutionContent {
9
+ export type Source = {
10
+ mediaType: string
11
+ bytes: number
12
+ status: "partial" | "complete"
13
+ contentVersion: string
14
+ sha256: string | null
15
+ read: (offset: number, limit: number) => Promise<z.infer<typeof RolloutEvidence.Content>>
16
+ stream: () => AsyncIterable<Uint8Array>
17
+ }
18
+ export const Query = z.object({
19
+ field: z.string().min(1).max(100),
20
+ runID: z.string().optional(),
21
+ version: z.string().max(4096).optional(),
22
+ })
23
+ export const PageQuery = Query.extend({
24
+ cursor: z.string().max(4096).optional(),
25
+ limit: z.coerce.number().int().min(1).max(500).default(100),
26
+ })
27
+ export const SearchQuery = PageQuery.extend({
28
+ query: z.string().min(1).max(500),
29
+ caseSensitive: z.preprocess((value) => (value === "false" ? false : value), z.coerce.boolean()).default(true),
30
+ })
31
+ export const Section = z.object({
32
+ path: z.array(z.string()),
33
+ kind: z.enum(["object", "array", "string", "number", "boolean", "null"]),
34
+ offset: z.number(),
35
+ bytes: z.number(),
36
+ role: z.string().optional(),
37
+ preview: z.string(),
38
+ })
39
+ export const Sections = z
40
+ .object({
41
+ contentVersion: z.string(),
42
+ format: z.enum(["json", "text", "partial"]),
43
+ items: z.array(Section),
44
+ total: z.number(),
45
+ nextCursor: z.string().nullable(),
46
+ truncated: z.boolean(),
47
+ })
48
+ .meta({ ref: "ExecutionContentSections" })
49
+ export const Search = z
50
+ .object({
51
+ contentVersion: z.string(),
52
+ status: z.enum(["partial", "complete"]),
53
+ items: z.array(z.object({ offset: z.number(), bytes: z.number(), preview: z.string() })),
54
+ nextCursor: z.string().nullable(),
55
+ })
56
+ .meta({ ref: "ExecutionContentSearch" })
57
+ const cache = RuntimeContext.state(() => ({
58
+ entries: new Map<string, { value: Awaited<ReturnType<typeof ExecutionJson.index>>; bytes: number }>(),
59
+ bytes: 0,
60
+ }))
61
+ function cursor(version: string, query: string, offset: number) {
62
+ return Buffer.from(JSON.stringify({ version, query, offset })).toString("base64url")
63
+ }
64
+ function position(input: string | undefined, version: string, query: string) {
65
+ if (!input) return 0
66
+ const value = queryCursor(
67
+ z.object({ version: z.string(), query: z.string(), offset: z.number().int().nonnegative() }),
68
+ input,
69
+ )
70
+ if (value.version !== version || value.query !== query)
71
+ throw new ExecutionQueryError("Execution content cursor does not match its version and query")
72
+ return value.offset
73
+ }
74
+ export async function sections(source: Source, input: z.infer<typeof PageQuery>, signal?: AbortSignal) {
75
+ const offset = position(input.cursor, source.contentVersion, "sections")
76
+ if (source.status === "partial" || !source.mediaType.includes("json"))
77
+ return Sections.parse({
78
+ contentVersion: source.contentVersion,
79
+ format: source.status === "partial" ? "partial" : "text",
80
+ items: [],
81
+ total: 0,
82
+ nextCursor: null,
83
+ truncated: false,
84
+ })
85
+ const state = cache()
86
+ const key = ScopeContext.current.scope.id + ":" + source.contentVersion
87
+ let value = state.entries.get(key)?.value
88
+ if (!value) {
89
+ try {
90
+ value = await ExecutionJson.index(source.stream(), signal)
91
+ } catch (error) {
92
+ if (!(error instanceof SyntaxError)) throw error
93
+ return Sections.parse({
94
+ contentVersion: source.contentVersion,
95
+ format: "text",
96
+ items: [],
97
+ total: 0,
98
+ nextCursor: null,
99
+ truncated: false,
100
+ })
101
+ }
102
+ const bytes = Buffer.byteLength(JSON.stringify(value))
103
+ if (!state.entries.has(key) && bytes <= 8 * 1024 * 1024) {
104
+ while (state.bytes + bytes > 8 * 1024 * 1024 && state.entries.size) {
105
+ const [key, entry] = state.entries.entries().next().value!
106
+ state.entries.delete(key)
107
+ state.bytes -= entry.bytes
108
+ }
109
+ state.entries.set(key, { value, bytes })
110
+ state.bytes += bytes
111
+ }
112
+ }
113
+ const items = value.items.slice(offset, offset + input.limit)
114
+ return Sections.parse({
115
+ contentVersion: source.contentVersion,
116
+ format: "json",
117
+ items,
118
+ total: value.items.length,
119
+ nextCursor:
120
+ offset + items.length < value.items.length
121
+ ? cursor(source.contentVersion, "sections", offset + items.length)
122
+ : null,
123
+ truncated: value.truncated,
124
+ })
125
+ }
126
+ export async function value(source: Source, path: string[], signal?: AbortSignal): Promise<unknown> {
127
+ let next: string | undefined
128
+ let section: z.infer<typeof Section> | undefined
129
+ do {
130
+ const page = await sections(source, PageQuery.parse({ field: "request", cursor: next, limit: 500 }), signal)
131
+ section = page.items.find((item) => JSON.stringify(item.path) === JSON.stringify(path))
132
+ next = page.nextCursor ?? undefined
133
+ } while (!section && next)
134
+ if (!section || section.bytes > 8 * 1024 * 1024) return undefined
135
+ const parts: string[] = []
136
+ for (let offset = section.offset; offset < section.offset + section.bytes;) {
137
+ signal?.throwIfAborted()
138
+ const page = await source.read(offset, Math.min(65_536, section.offset + section.bytes - offset))
139
+ parts.push(page.text)
140
+ offset = page.nextOffset ?? source.bytes
141
+ }
142
+ return JSON.parse(parts.join(""))
143
+ }
144
+ export async function search(source: Source, input: z.infer<typeof SearchQuery>, signal?: AbortSignal) {
145
+ const fingerprint = JSON.stringify([input.query, input.caseSensitive])
146
+ let offset = position(input.cursor, source.contentVersion, fingerprint)
147
+ const pattern = new RegExp(input.query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), input.caseSensitive ? "gu" : "giu")
148
+ const items: Array<{ offset: number; bytes: number; preview: string }> = []
149
+ let carry = ""
150
+ let carryOffset = offset
151
+ let next: number | null = null
152
+ while (offset < source.bytes) {
153
+ signal?.throwIfAborted()
154
+ const page = await source.read(offset, 65_536)
155
+ const text = carry + page.text
156
+ const base = carry ? carryOffset : offset
157
+ const safe = page.nextOffset === null ? text.length : Math.max(0, text.length - input.query.length - 2)
158
+ pattern.lastIndex = 0
159
+ let consumed = 0
160
+ for (let match = pattern.exec(text); match && match.index < safe; match = pattern.exec(text)) {
161
+ const start = base + Buffer.byteLength(text.slice(0, match.index))
162
+ const bytes = Buffer.byteLength(match[0])
163
+ items.push({
164
+ offset: start,
165
+ bytes,
166
+ preview: text.slice(Math.max(0, match.index - 60), match.index + match[0].length + 100),
167
+ })
168
+ consumed = match.index + match[0].length
169
+ if (items.length === input.limit) {
170
+ next = start + bytes
171
+ break
172
+ }
173
+ }
174
+ if (next !== null) break
175
+ let retained = Math.max(safe, consumed)
176
+ if (retained && retained < text.length && /[\uDC00-\uDFFF]/.test(text[retained])) retained--
177
+ carry = text.slice(retained)
178
+ carryOffset = base + Buffer.byteLength(text.slice(0, retained))
179
+ offset = page.nextOffset ?? source.bytes
180
+ await new Promise<void>((resolve) => setImmediate(resolve))
181
+ }
182
+ return Search.parse({
183
+ contentVersion: source.contentVersion,
184
+ status: source.status,
185
+ items,
186
+ nextCursor: next !== null && next < source.bytes ? cursor(source.contentVersion, fingerprint, next) : null,
187
+ })
188
+ }
189
+ }