opensteer 0.9.2 → 0.9.4

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 (39) hide show
  1. package/README.md +158 -165
  2. package/dist/{chunk-HD6KVZ42.js → chunk-GEUHKPC2.js} +46 -16
  3. package/dist/chunk-GEUHKPC2.js.map +1 -0
  4. package/dist/{chunk-2TIVULZY.js → chunk-GSCQQKZZ.js} +53 -9
  5. package/dist/chunk-GSCQQKZZ.js.map +1 -0
  6. package/dist/{chunk-KPYLS2KQ.js → chunk-HQCMXRBE.js} +5 -4
  7. package/dist/chunk-HQCMXRBE.js.map +1 -0
  8. package/dist/{chunk-BMPUL66S.js → chunk-T5P2QGZ3.js} +58 -53
  9. package/dist/chunk-T5P2QGZ3.js.map +1 -0
  10. package/dist/{chunk-FIMNKEG5.js → chunk-ZRF7WMS3.js} +4 -4
  11. package/dist/{chunk-FIMNKEG5.js.map → chunk-ZRF7WMS3.js.map} +1 -1
  12. package/dist/cli/bin.cjs +160 -72
  13. package/dist/cli/bin.cjs.map +1 -1
  14. package/dist/cli/bin.js +17 -7
  15. package/dist/cli/bin.js.map +1 -1
  16. package/dist/index.cjs +149 -69
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +19 -2
  19. package/dist/index.d.ts +19 -2
  20. package/dist/index.js +4 -4
  21. package/dist/local-view/public/assets/app.css +219 -55
  22. package/dist/local-view/public/assets/app.js +58 -2
  23. package/dist/local-view/public/index.html +101 -26
  24. package/dist/local-view/serve-entry.cjs +106 -57
  25. package/dist/local-view/serve-entry.cjs.map +1 -1
  26. package/dist/local-view/serve-entry.js +2 -2
  27. package/dist/opensteer-PJI7VUIT.js +6 -0
  28. package/dist/{opensteer-MIQ43CY4.js.map → opensteer-PJI7VUIT.js.map} +1 -1
  29. package/dist/{session-control-IFE3IPS3.js → session-control-M3JD7ZKA.js} +4 -4
  30. package/dist/{session-control-IFE3IPS3.js.map → session-control-M3JD7ZKA.js.map} +1 -1
  31. package/package.json +5 -5
  32. package/skills/opensteer/SKILL.md +7 -8
  33. package/skills/recorder/SKILL.md +43 -48
  34. package/dist/chunk-2TIVULZY.js.map +0 -1
  35. package/dist/chunk-BMPUL66S.js.map +0 -1
  36. package/dist/chunk-HD6KVZ42.js.map +0 -1
  37. package/dist/chunk-KPYLS2KQ.js.map +0 -1
  38. package/dist/opensteer-MIQ43CY4.js +0 -6
  39. package/skills/recorder/references/recorder-reference.md +0 -71
package/dist/index.d.cts CHANGED
@@ -1637,6 +1637,14 @@ interface OpensteerAttachBrowserOptions {
1637
1637
  }
1638
1638
  type OpensteerBrowserMode = "temporary" | "persistent";
1639
1639
  type OpensteerBrowserOptions = OpensteerBrowserMode | OpensteerAttachBrowserOptions;
1640
+ interface OpensteerHumanizeOptions {
1641
+ /** Interpolate mouse paths with curves and jitter before clicks/scrolls. */
1642
+ readonly mouse?: boolean;
1643
+ /** Realistic per-character typing cadence with key hold duration. */
1644
+ readonly keyboard?: boolean;
1645
+ /** Break large scrolls into discrete wheel ticks with delays. */
1646
+ readonly scroll?: boolean;
1647
+ }
1640
1648
  interface OpensteerBrowserContextOptions {
1641
1649
  readonly ignoreHTTPSErrors?: boolean;
1642
1650
  readonly locale?: string;
@@ -1651,6 +1659,12 @@ interface OpensteerBrowserContextOptions {
1651
1659
  readonly reducedMotion?: "reduce" | "no-preference";
1652
1660
  readonly colorScheme?: "light" | "dark" | "no-preference";
1653
1661
  readonly stealthProfile?: OpensteerStealthProfileInput;
1662
+ /**
1663
+ * Enable human-like interaction patterns for mouse, keyboard, and scroll
1664
+ * events. When `true` (the default), all sub-options are enabled. Pass
1665
+ * `false` to disable, or an object to control individual categories.
1666
+ */
1667
+ readonly humanize?: boolean | OpensteerHumanizeOptions;
1654
1668
  }
1655
1669
  interface OpensteerStealthProfileInput {
1656
1670
  readonly id?: string;
@@ -2958,6 +2972,8 @@ declare function createOpensteerExtractionDescriptorStore(options: {
2958
2972
  }): OpensteerExtractionDescriptorStore;
2959
2973
  declare function parseExtractionDescriptorRecord(record: DescriptorRecord): OpensteerExtractionDescriptorRecord | undefined;
2960
2974
 
2975
+ type OpensteerEnvironment = Record<string, string | undefined>;
2976
+
2961
2977
  declare const OPENSTEER_ENGINE_NAMES: readonly ["playwright", "abp"];
2962
2978
  type OpensteerEngineName = (typeof OPENSTEER_ENGINE_NAMES)[number];
2963
2979
  declare const DEFAULT_OPENSTEER_ENGINE: OpensteerEngineName;
@@ -3006,6 +3022,7 @@ interface OpensteerBrowserManagerOptions {
3006
3022
  readonly rootPath?: string;
3007
3023
  readonly workspace?: string;
3008
3024
  readonly engineName?: OpensteerEngineName;
3025
+ readonly environment?: OpensteerEnvironment;
3009
3026
  readonly browser?: OpensteerBrowserOptions;
3010
3027
  readonly launch?: OpensteerBrowserLaunchOptions;
3011
3028
  readonly context?: OpensteerBrowserContextOptions;
@@ -3139,6 +3156,7 @@ interface OpensteerRuntimeOptions {
3139
3156
  readonly rootDir?: string;
3140
3157
  readonly rootPath?: string;
3141
3158
  readonly engineName?: OpensteerEngineName;
3159
+ readonly environment?: OpensteerEnvironment;
3142
3160
  readonly browser?: OpensteerBrowserOptions;
3143
3161
  readonly launch?: OpensteerBrowserLaunchOptions;
3144
3162
  readonly context?: OpensteerBrowserContextOptions;
@@ -3157,6 +3175,7 @@ interface OpensteerSessionRuntimeOptions {
3157
3175
  readonly rootDir?: string;
3158
3176
  readonly rootPath?: string;
3159
3177
  readonly engineName?: OpensteerEngineName;
3178
+ readonly environment?: OpensteerEnvironment;
3160
3179
  readonly browser?: OpensteerBrowserOptions;
3161
3180
  readonly launch?: OpensteerBrowserLaunchOptions;
3162
3181
  readonly context?: OpensteerBrowserContextOptions;
@@ -3287,8 +3306,6 @@ declare class Opensteer {
3287
3306
  private requireOwnedInstrumentationRuntime;
3288
3307
  }
3289
3308
 
3290
- type OpensteerEnvironment = Record<string, string | undefined>;
3291
-
3292
3309
  interface OpensteerCloudConfig {
3293
3310
  readonly apiKey: string;
3294
3311
  readonly baseUrl: string;
package/dist/index.d.ts CHANGED
@@ -1637,6 +1637,14 @@ interface OpensteerAttachBrowserOptions {
1637
1637
  }
1638
1638
  type OpensteerBrowserMode = "temporary" | "persistent";
1639
1639
  type OpensteerBrowserOptions = OpensteerBrowserMode | OpensteerAttachBrowserOptions;
1640
+ interface OpensteerHumanizeOptions {
1641
+ /** Interpolate mouse paths with curves and jitter before clicks/scrolls. */
1642
+ readonly mouse?: boolean;
1643
+ /** Realistic per-character typing cadence with key hold duration. */
1644
+ readonly keyboard?: boolean;
1645
+ /** Break large scrolls into discrete wheel ticks with delays. */
1646
+ readonly scroll?: boolean;
1647
+ }
1640
1648
  interface OpensteerBrowserContextOptions {
1641
1649
  readonly ignoreHTTPSErrors?: boolean;
1642
1650
  readonly locale?: string;
@@ -1651,6 +1659,12 @@ interface OpensteerBrowserContextOptions {
1651
1659
  readonly reducedMotion?: "reduce" | "no-preference";
1652
1660
  readonly colorScheme?: "light" | "dark" | "no-preference";
1653
1661
  readonly stealthProfile?: OpensteerStealthProfileInput;
1662
+ /**
1663
+ * Enable human-like interaction patterns for mouse, keyboard, and scroll
1664
+ * events. When `true` (the default), all sub-options are enabled. Pass
1665
+ * `false` to disable, or an object to control individual categories.
1666
+ */
1667
+ readonly humanize?: boolean | OpensteerHumanizeOptions;
1654
1668
  }
1655
1669
  interface OpensteerStealthProfileInput {
1656
1670
  readonly id?: string;
@@ -2958,6 +2972,8 @@ declare function createOpensteerExtractionDescriptorStore(options: {
2958
2972
  }): OpensteerExtractionDescriptorStore;
2959
2973
  declare function parseExtractionDescriptorRecord(record: DescriptorRecord): OpensteerExtractionDescriptorRecord | undefined;
2960
2974
 
2975
+ type OpensteerEnvironment = Record<string, string | undefined>;
2976
+
2961
2977
  declare const OPENSTEER_ENGINE_NAMES: readonly ["playwright", "abp"];
2962
2978
  type OpensteerEngineName = (typeof OPENSTEER_ENGINE_NAMES)[number];
2963
2979
  declare const DEFAULT_OPENSTEER_ENGINE: OpensteerEngineName;
@@ -3006,6 +3022,7 @@ interface OpensteerBrowserManagerOptions {
3006
3022
  readonly rootPath?: string;
3007
3023
  readonly workspace?: string;
3008
3024
  readonly engineName?: OpensteerEngineName;
3025
+ readonly environment?: OpensteerEnvironment;
3009
3026
  readonly browser?: OpensteerBrowserOptions;
3010
3027
  readonly launch?: OpensteerBrowserLaunchOptions;
3011
3028
  readonly context?: OpensteerBrowserContextOptions;
@@ -3139,6 +3156,7 @@ interface OpensteerRuntimeOptions {
3139
3156
  readonly rootDir?: string;
3140
3157
  readonly rootPath?: string;
3141
3158
  readonly engineName?: OpensteerEngineName;
3159
+ readonly environment?: OpensteerEnvironment;
3142
3160
  readonly browser?: OpensteerBrowserOptions;
3143
3161
  readonly launch?: OpensteerBrowserLaunchOptions;
3144
3162
  readonly context?: OpensteerBrowserContextOptions;
@@ -3157,6 +3175,7 @@ interface OpensteerSessionRuntimeOptions {
3157
3175
  readonly rootDir?: string;
3158
3176
  readonly rootPath?: string;
3159
3177
  readonly engineName?: OpensteerEngineName;
3178
+ readonly environment?: OpensteerEnvironment;
3160
3179
  readonly browser?: OpensteerBrowserOptions;
3161
3180
  readonly launch?: OpensteerBrowserLaunchOptions;
3162
3181
  readonly context?: OpensteerBrowserContextOptions;
@@ -3287,8 +3306,6 @@ declare class Opensteer {
3287
3306
  private requireOwnedInstrumentationRuntime;
3288
3307
  }
3289
3308
 
3290
- type OpensteerEnvironment = Record<string, string | undefined>;
3291
-
3292
3309
  interface OpensteerCloudConfig {
3293
3310
  readonly apiKey: string;
3294
3311
  readonly baseUrl: string;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import './chunk-KCINASQC.js';
2
- export { Opensteer } from './chunk-KPYLS2KQ.js';
3
- export { CloudSessionProxy, DEFERRED_MATCH_ATTR_KEYS, ElementPathError, MATCH_ATTRIBUTE_PRIORITY, OPENSTEER_DOM_ACTION_BRIDGE_SYMBOL, OpensteerCloudClient, OpensteerRuntime, OpensteerSessionRuntime, STABLE_PRIMARY_ATTR_KEYS, assertProviderSupportsEngine, buildArrayFieldPathCandidates, buildDomDescriptorKey, buildDomDescriptorPayload, buildDomDescriptorVersion, buildPathCandidates, buildPathSelectorHint, buildSegmentSelector, cloneElementPath, cloneReplayElementPath, cloneStructuralElementAnchor, createDomDescriptorStore, createDomRuntime, createOpensteerExtractionDescriptorStore, createOpensteerSemanticRuntime, defaultFallbackPolicy, defaultPolicy, defaultRetryPolicy, defaultSettlePolicy, defaultTimeoutPolicy, delayWithSignal, dispatchSemanticOperation, hashDomDescriptorPersist, isCurrentUrlField, isValidCssAttributeKey, normalizeExtractedValue, normalizeOpensteerProviderMode, parseDomDescriptorRecord, parseExtractionDescriptorRecord, resolveCloudConfig, resolveDomActionBridge, resolveExtractedValueInContext, resolveOpensteerProvider, resolveOpensteerRuntimeConfig, runWithPolicyTimeout, sanitizeElementPath, sanitizeReplayElementPath, sanitizeStructuralElementAnchor, settleWithPolicy, shouldKeepAttributeForPath } from './chunk-HD6KVZ42.js';
4
- export { DEFAULT_OPENSTEER_ENGINE, OPENSTEER_ENGINE_NAMES, OPENSTEER_FILESYSTEM_WORKSPACE_LAYOUT, OPENSTEER_FILESYSTEM_WORKSPACE_VERSION, OpensteerBrowserManager, createArtifactStore, createFilesystemOpensteerWorkspace, createObservationStore, manifestToExternalBinaryLocation, normalizeObservabilityConfig, normalizeOpensteerEngineName, normalizeWorkspaceId, resolveFilesystemWorkspacePath, resolveOpensteerEngineName } from './chunk-2TIVULZY.js';
5
- export { OpensteerAttachAmbiguousError, clearPersistedSessionRecord, discoverLocalCdpBrowsers, inspectCdpEndpoint, listLocalChromeProfiles, readPersistedCloudSessionRecord, readPersistedLocalBrowserSessionRecord, readPersistedSessionRecord, resolveCloudSessionRecordPath, resolveLiveSessionRecordPath, resolveLocalSessionRecordPath, writePersistedSessionRecord } from './chunk-BMPUL66S.js';
2
+ export { Opensteer } from './chunk-HQCMXRBE.js';
3
+ export { CloudSessionProxy, DEFERRED_MATCH_ATTR_KEYS, ElementPathError, MATCH_ATTRIBUTE_PRIORITY, OPENSTEER_DOM_ACTION_BRIDGE_SYMBOL, OpensteerCloudClient, OpensteerRuntime, OpensteerSessionRuntime, STABLE_PRIMARY_ATTR_KEYS, assertProviderSupportsEngine, buildArrayFieldPathCandidates, buildDomDescriptorKey, buildDomDescriptorPayload, buildDomDescriptorVersion, buildPathCandidates, buildPathSelectorHint, buildSegmentSelector, cloneElementPath, cloneReplayElementPath, cloneStructuralElementAnchor, createDomDescriptorStore, createDomRuntime, createOpensteerExtractionDescriptorStore, createOpensteerSemanticRuntime, defaultFallbackPolicy, defaultPolicy, defaultRetryPolicy, defaultSettlePolicy, defaultTimeoutPolicy, delayWithSignal, dispatchSemanticOperation, hashDomDescriptorPersist, isCurrentUrlField, isValidCssAttributeKey, normalizeExtractedValue, normalizeOpensteerProviderMode, parseDomDescriptorRecord, parseExtractionDescriptorRecord, resolveCloudConfig, resolveDomActionBridge, resolveExtractedValueInContext, resolveOpensteerProvider, resolveOpensteerRuntimeConfig, runWithPolicyTimeout, sanitizeElementPath, sanitizeReplayElementPath, sanitizeStructuralElementAnchor, settleWithPolicy, shouldKeepAttributeForPath } from './chunk-GEUHKPC2.js';
4
+ export { DEFAULT_OPENSTEER_ENGINE, OPENSTEER_ENGINE_NAMES, OPENSTEER_FILESYSTEM_WORKSPACE_LAYOUT, OPENSTEER_FILESYSTEM_WORKSPACE_VERSION, OpensteerBrowserManager, createArtifactStore, createFilesystemOpensteerWorkspace, createObservationStore, manifestToExternalBinaryLocation, normalizeObservabilityConfig, normalizeOpensteerEngineName, normalizeWorkspaceId, resolveFilesystemWorkspacePath, resolveOpensteerEngineName } from './chunk-GSCQQKZZ.js';
5
+ export { OpensteerAttachAmbiguousError, clearPersistedSessionRecord, discoverLocalCdpBrowsers, inspectCdpEndpoint, listLocalChromeProfiles, readPersistedCloudSessionRecord, readPersistedLocalBrowserSessionRecord, readPersistedSessionRecord, resolveCloudSessionRecordPath, resolveLiveSessionRecordPath, resolveLocalSessionRecordPath, writePersistedSessionRecord } from './chunk-T5P2QGZ3.js';
6
6
  //# sourceMappingURL=index.js.map
7
7
  //# sourceMappingURL=index.js.map
@@ -16,6 +16,7 @@
16
16
  --accent-foreground: #000000;
17
17
  --destructive: #ef4444;
18
18
  --ring: rgba(255, 255, 255, 0.4);
19
+ --top-bar-height: 44px;
19
20
  }
20
21
 
21
22
  /* ─── Reset ─── */
@@ -34,14 +35,7 @@ body {
34
35
  overflow: hidden;
35
36
  background: var(--background);
36
37
  color: var(--foreground);
37
- font-family:
38
- Inter,
39
- ui-sans-serif,
40
- system-ui,
41
- -apple-system,
42
- BlinkMacSystemFont,
43
- "Segoe UI",
44
- sans-serif;
38
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif;
45
39
  font-size: 14px;
46
40
  line-height: 1.5;
47
41
  -webkit-font-smoothing: antialiased;
@@ -124,34 +118,78 @@ button {
124
118
  height: 100vh;
125
119
  min-height: 0;
126
120
  display: flex;
121
+ flex-direction: column;
127
122
  overflow: hidden;
128
123
  }
129
124
 
130
- /* ─── Sidebar ─── */
125
+ /* ─── Top Bar ─── */
131
126
 
132
- .sidebar {
133
- width: 280px;
127
+ .top-bar {
128
+ height: var(--top-bar-height);
134
129
  flex-shrink: 0;
135
130
  display: flex;
136
- flex-direction: column;
137
- overflow: hidden;
138
- border-right: 1px solid var(--border);
131
+ align-items: center;
132
+ gap: 12px;
133
+ padding: 0 16px;
139
134
  background: var(--surface);
135
+ border-bottom: 1px solid var(--border);
136
+ z-index: 50;
137
+ position: relative;
138
+ }
139
+
140
+ .drawer-toggle {
141
+ position: relative;
142
+ display: inline-flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ width: 32px;
146
+ height: 32px;
147
+ border: none;
148
+ border-radius: 6px;
149
+ background: transparent;
150
+ color: var(--muted-foreground);
151
+ padding: 0;
152
+ flex-shrink: 0;
153
+ transition:
154
+ background 150ms ease,
155
+ color 150ms ease;
156
+ }
157
+
158
+ .drawer-toggle:hover {
159
+ background: var(--muted);
160
+ color: var(--foreground);
140
161
  }
141
162
 
142
- .brand-block {
163
+ .drawer-toggle:active {
164
+ background: var(--border-strong);
165
+ }
166
+
167
+ .session-count-badge {
168
+ position: absolute;
169
+ top: 1px;
170
+ right: 1px;
171
+ min-width: 16px;
172
+ height: 16px;
173
+ border-radius: 8px;
174
+ background: var(--accent);
175
+ color: var(--accent-foreground);
176
+ font-size: 9px;
177
+ font-weight: 600;
143
178
  display: flex;
144
179
  align-items: center;
145
- justify-content: space-between;
146
- gap: 12px;
147
- padding: 16px 16px 14px;
148
- border-bottom: 1px solid var(--border);
180
+ justify-content: center;
181
+ padding: 0 4px;
182
+ line-height: 1;
183
+ pointer-events: none;
149
184
  }
150
185
 
186
+ /* ─── Brand ─── */
187
+
151
188
  .brand-row {
152
189
  display: inline-flex;
153
190
  align-items: center;
154
191
  gap: 8px;
192
+ flex-shrink: 0;
155
193
  }
156
194
 
157
195
  .brand-icon {
@@ -182,6 +220,80 @@ button {
182
220
  letter-spacing: 0.04em;
183
221
  }
184
222
 
223
+ .top-bar-divider {
224
+ width: 1px;
225
+ height: 20px;
226
+ background: var(--border-strong);
227
+ flex-shrink: 0;
228
+ }
229
+
230
+ /* ─── Active Session Indicator ─── */
231
+
232
+ .active-session-indicator {
233
+ display: inline-flex;
234
+ align-items: center;
235
+ gap: 8px;
236
+ border: 1px solid var(--border);
237
+ border-radius: 8px;
238
+ background: var(--surface-elevated);
239
+ color: var(--foreground);
240
+ padding: 5px 12px;
241
+ font-size: 12px;
242
+ font-weight: 500;
243
+ white-space: nowrap;
244
+ overflow: hidden;
245
+ max-width: 300px;
246
+ transition:
247
+ background 150ms ease,
248
+ border-color 150ms ease;
249
+ }
250
+
251
+ .active-session-indicator:hover {
252
+ background: var(--surface-raised);
253
+ border-color: var(--border-strong);
254
+ }
255
+
256
+ .active-session-indicator:active {
257
+ background: rgba(255, 255, 255, 0.08);
258
+ }
259
+
260
+ .active-session-dot {
261
+ flex-shrink: 0;
262
+ width: 6px;
263
+ height: 6px;
264
+ border-radius: 50%;
265
+ background: var(--muted-foreground);
266
+ opacity: 0.4;
267
+ transition: all 200ms ease;
268
+ }
269
+
270
+ .active-session-dot.is-active {
271
+ background: #28c840;
272
+ opacity: 1;
273
+ animation: pulse-dot 1.5s ease-in-out infinite;
274
+ }
275
+
276
+ .active-session-label {
277
+ overflow: hidden;
278
+ text-overflow: ellipsis;
279
+ white-space: nowrap;
280
+ min-width: 0;
281
+ }
282
+
283
+ .active-session-chevron {
284
+ flex-shrink: 0;
285
+ color: var(--muted-foreground);
286
+ opacity: 0.5;
287
+ transition: transform 200ms ease;
288
+ }
289
+
290
+ .top-bar-spacer {
291
+ flex: 1;
292
+ min-width: 0;
293
+ }
294
+
295
+ /* ─── View Toggle ─── */
296
+
185
297
  .local-view-toggle {
186
298
  flex-shrink: 0;
187
299
  height: 26px;
@@ -211,52 +323,100 @@ button {
211
323
  opacity: 0.6;
212
324
  }
213
325
 
214
- /* ─── Sidebar Tabs ─── */
326
+ /* ─── Session Drawer ─── */
215
327
 
216
- .sidebar-tab-bar {
217
- padding: 12px 16px;
218
- border-bottom: 1px solid var(--border);
328
+ .drawer-backdrop {
329
+ position: fixed;
330
+ top: var(--top-bar-height);
331
+ left: 0;
332
+ right: 0;
333
+ bottom: 0;
334
+ background: rgba(0, 0, 0, 0.5);
335
+ backdrop-filter: blur(4px);
336
+ -webkit-backdrop-filter: blur(4px);
337
+ opacity: 0;
338
+ pointer-events: none;
339
+ transition: opacity 280ms ease;
340
+ z-index: 90;
341
+ }
342
+
343
+ .drawer-backdrop.is-visible {
344
+ opacity: 1;
345
+ pointer-events: auto;
219
346
  }
220
347
 
221
- .sidebar-tab-bar-inner {
348
+ .session-drawer {
349
+ position: fixed;
350
+ top: var(--top-bar-height);
351
+ left: 0;
352
+ bottom: 0;
353
+ width: 320px;
222
354
  display: flex;
223
- border-radius: 8px;
224
- border: 1px solid rgba(255, 255, 255, 0.06);
225
- background: var(--surface-elevated);
226
- padding: 3px;
355
+ flex-direction: column;
356
+ background: var(--surface);
357
+ border-right: 1px solid var(--border);
358
+ transform: translateX(-100%);
359
+ transition:
360
+ transform 280ms cubic-bezier(0, 0, 0.2, 1),
361
+ box-shadow 280ms ease;
362
+ z-index: 100;
363
+ overflow: hidden;
227
364
  }
228
365
 
229
- .sidebar-tab {
230
- flex: 1;
366
+ .session-drawer.is-open {
367
+ transform: translateX(0);
368
+ box-shadow: 8px 0 32px rgba(0, 0, 0, 0.4);
369
+ }
370
+
371
+ .drawer-header {
372
+ display: flex;
373
+ align-items: center;
374
+ justify-content: space-between;
375
+ padding: 14px 16px;
376
+ border-bottom: 1px solid var(--border);
377
+ flex-shrink: 0;
378
+ }
379
+
380
+ .drawer-title {
381
+ font-size: 13px;
382
+ font-weight: 600;
383
+ color: var(--foreground);
384
+ letter-spacing: 0.02em;
385
+ }
386
+
387
+ .drawer-close {
388
+ display: inline-flex;
389
+ align-items: center;
390
+ justify-content: center;
391
+ width: 28px;
392
+ height: 28px;
231
393
  border: none;
232
394
  border-radius: 6px;
233
- padding: 6px 12px;
234
- font-size: 12px;
235
- font-weight: 500;
236
- color: var(--muted-foreground);
237
395
  background: transparent;
238
- transition: all 200ms;
396
+ color: var(--muted-foreground);
397
+ padding: 0;
398
+ transition:
399
+ background 150ms ease,
400
+ color 150ms ease;
239
401
  }
240
402
 
241
- .sidebar-tab:hover {
242
- background: rgba(255, 255, 255, 0.03);
403
+ .drawer-close:hover {
404
+ background: var(--muted);
243
405
  color: var(--foreground);
244
406
  }
245
407
 
246
- .sidebar-tab.is-active {
247
- background: var(--surface);
248
- color: var(--foreground);
249
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
408
+ .drawer-close:active {
409
+ background: var(--border-strong);
250
410
  }
251
411
 
252
- /* ─── Session List ─── */
253
-
254
- .sidebar-scroll {
412
+ .drawer-scroll {
255
413
  flex: 1;
256
414
  min-height: 0;
257
415
  overflow-y: auto;
258
416
  }
259
417
 
418
+ /* ─── Session List ─── */
419
+
260
420
  .session-list {
261
421
  display: flex;
262
422
  flex-direction: column;
@@ -383,7 +543,7 @@ button {
383
543
  display: flex;
384
544
  align-items: flex-start;
385
545
  justify-content: center;
386
- padding: 20px;
546
+ padding: 16px;
387
547
  min-height: 0;
388
548
  overflow: hidden;
389
549
  }
@@ -750,21 +910,25 @@ button {
750
910
  /* ─── Responsive ─── */
751
911
 
752
912
  @media (max-width: 768px) {
753
- .app-shell {
754
- flex-direction: column;
913
+ .session-drawer {
914
+ width: 100%;
755
915
  }
756
916
 
757
- .sidebar {
758
- width: 100%;
759
- border-right: none;
760
- border-bottom: 1px solid var(--border);
917
+ .viewer-area {
918
+ padding: 8px;
761
919
  }
920
+ }
762
921
 
763
- .sidebar-scroll {
764
- max-height: 200px;
922
+ @media (max-width: 640px) {
923
+ .brand-name {
924
+ display: none;
765
925
  }
766
926
 
767
- .viewer-area {
768
- padding: 12px;
927
+ .top-bar-divider {
928
+ display: none;
929
+ }
930
+
931
+ .active-session-indicator {
932
+ max-width: 180px;
769
933
  }
770
934
  }
@@ -956,6 +956,7 @@ class LocalViewApp {
956
956
  this.layoutFrame = null;
957
957
  this.lastBrowserFrameWidth = null;
958
958
  this.lastStreamAspect = null;
959
+ this.drawerOpen = false;
959
960
 
960
961
  this.viewerAreaEl = document.querySelector(".viewer-area");
961
962
  this.browserFrameEl = document.querySelector(".browser-frame");
@@ -978,6 +979,14 @@ class LocalViewApp {
978
979
  this.newTabButtonEl = document.getElementById("new-tab-button");
979
980
  this.closeBrowserButtonEl = document.getElementById("close-browser-button");
980
981
  this.stopViewButtonEl = document.getElementById("stop-view-button");
982
+ this.drawerToggleEl = document.getElementById("drawer-toggle");
983
+ this.drawerCloseEl = document.getElementById("drawer-close");
984
+ this.drawerBackdropEl = document.getElementById("drawer-backdrop");
985
+ this.sessionDrawerEl = document.getElementById("session-drawer");
986
+ this.activeSessionIndicatorEl = document.getElementById("active-session-indicator");
987
+ this.activeSessionLabelEl = document.getElementById("active-session-label");
988
+ this.activeSessionDotEl = document.getElementById("active-session-dot");
989
+ this.sessionCountBadgeEl = document.getElementById("session-count-badge");
981
990
 
982
991
  this.stream = new LocalBrowserStream(() => this.render());
983
992
  this.cdp = new LocalCdpConnection(() => this.render());
@@ -1007,6 +1016,30 @@ class LocalViewApp {
1007
1016
  }
1008
1017
  }
1009
1018
 
1019
+ openDrawer() {
1020
+ this.drawerOpen = true;
1021
+ this.sessionDrawerEl.classList.add("is-open");
1022
+ this.drawerBackdropEl.classList.add("is-visible");
1023
+ this.drawerToggleEl.setAttribute("aria-expanded", "true");
1024
+ this.activeSessionIndicatorEl.setAttribute("aria-expanded", "true");
1025
+ }
1026
+
1027
+ closeDrawer() {
1028
+ this.drawerOpen = false;
1029
+ this.sessionDrawerEl.classList.remove("is-open");
1030
+ this.drawerBackdropEl.classList.remove("is-visible");
1031
+ this.drawerToggleEl.setAttribute("aria-expanded", "false");
1032
+ this.activeSessionIndicatorEl.setAttribute("aria-expanded", "false");
1033
+ }
1034
+
1035
+ toggleDrawer() {
1036
+ if (this.drawerOpen) {
1037
+ this.closeDrawer();
1038
+ } else {
1039
+ this.openDrawer();
1040
+ }
1041
+ }
1042
+
1010
1043
  bindUi() {
1011
1044
  window.addEventListener("hashchange", () => {
1012
1045
  const hashSessionId = resolveSelectedSessionIdFromHash();
@@ -1015,12 +1048,24 @@ class LocalViewApp {
1015
1048
  }
1016
1049
  });
1017
1050
 
1051
+ this.drawerToggleEl.addEventListener("click", () => this.toggleDrawer());
1052
+ this.drawerCloseEl.addEventListener("click", () => this.closeDrawer());
1053
+ this.drawerBackdropEl.addEventListener("click", () => this.closeDrawer());
1054
+ this.activeSessionIndicatorEl.addEventListener("click", () => this.toggleDrawer());
1055
+
1056
+ document.addEventListener("keydown", (event) => {
1057
+ if (event.key === "Escape" && this.drawerOpen) {
1058
+ this.closeDrawer();
1059
+ }
1060
+ });
1061
+
1018
1062
  this.sessionListEl.addEventListener("click", (event) => {
1019
1063
  const button = event.target.closest("button[data-session-id]");
1020
1064
  if (!button) {
1021
1065
  return;
1022
1066
  }
1023
1067
  this.selectSession(button.dataset.sessionId ?? null);
1068
+ this.closeDrawer();
1024
1069
  });
1025
1070
 
1026
1071
  this.tabStripEl.addEventListener("click", (event) => {
@@ -1182,10 +1227,10 @@ class LocalViewApp {
1182
1227
  );
1183
1228
 
1184
1229
  this.viewerSurfaceEl.addEventListener("keydown", (event) => {
1185
- const payload = createCdpKeyDownPayload(event);
1186
- if (!payload) {
1230
+ if (event.key === "Escape" && this.drawerOpen) {
1187
1231
  return;
1188
1232
  }
1233
+ const payload = createCdpKeyDownPayload(event);
1189
1234
  event.preventDefault();
1190
1235
  void this.dispatchPointerCommand("Input.dispatchKeyEvent", payload);
1191
1236
  });
@@ -1460,6 +1505,17 @@ class LocalViewApp {
1460
1505
  }
1461
1506
 
1462
1507
  renderSessions() {
1508
+ const activeSession = this.sessions.find((s) => s.sessionId === this.selectedSessionId) ?? null;
1509
+ this.activeSessionLabelEl.textContent = activeSession
1510
+ ? activeSession.label
1511
+ : "No session selected";
1512
+ this.activeSessionDotEl.className = activeSession
1513
+ ? "active-session-dot is-active"
1514
+ : "active-session-dot";
1515
+ const sessionCount = this.sessions.length;
1516
+ this.sessionCountBadgeEl.textContent = String(sessionCount);
1517
+ this.sessionCountBadgeEl.hidden = sessionCount === 0;
1518
+
1463
1519
  this.sessionListEl.textContent = "";
1464
1520
  if (this.sessions.length === 0) {
1465
1521
  const empty = document.createElement("div");