@flowdular/sandbox 0.2.6 → 0.2.8

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.
@@ -0,0 +1,26 @@
1
+ import { resolve } from 'node:path';
2
+
3
+ /* Session metadata and operator-owned attachment snapshots share one queue.
4
+ Failed work releases the next caller and idle sessions retain no lock. */
5
+ const pending = new Map<string, Promise<void>>();
6
+
7
+ export async function withSessionLock<T>(
8
+ workspaceRoot: string,
9
+ sessionId: string,
10
+ work: () => Promise<T>,
11
+ ): Promise<T> {
12
+ const key = `${resolve(workspaceRoot)}\0${sessionId}`;
13
+ const previous = pending.get(key);
14
+ let release!: () => void;
15
+ const current = new Promise<void>((done) => {
16
+ release = done;
17
+ });
18
+ pending.set(key, current);
19
+ await previous;
20
+ try {
21
+ return await work();
22
+ } finally {
23
+ release();
24
+ if (pending.get(key) === current) pending.delete(key);
25
+ }
26
+ }
@@ -1,3 +1,4 @@
1
+ import { withSessionLock } from './session-lock.ts';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import {
3
4
  access,
@@ -588,16 +589,23 @@ export async function listSessions(
588
589
  return sessions.sort((left, right) => right.updatedAt - left.updatedAt);
589
590
  }
590
591
 
592
+ type SessionPatch = Partial<Omit<SandboxSession, 'id' | 'createdAt'>>;
593
+
591
594
  export async function updateSession(
592
595
  workspaceRoot: string,
593
596
  sessionId: string,
594
- patch: Partial<Omit<SandboxSession, 'id' | 'createdAt'>>,
597
+ patch:
598
+ | SessionPatch
599
+ | ((current: SandboxSession) => SessionPatch | Promise<SessionPatch>),
595
600
  ): Promise<SandboxSession> {
596
- const current = await readSession(workspaceRoot, sessionId);
597
- return writeSession(workspaceRoot, {
598
- ...current,
599
- ...patch,
600
- updatedAt: Date.now(),
601
+ return withSessionLock(workspaceRoot, sessionId, async () => {
602
+ const current = await readSession(workspaceRoot, sessionId);
603
+ const updates = typeof patch === 'function' ? await patch(current) : patch;
604
+ return writeSession(workspaceRoot, {
605
+ ...current,
606
+ ...updates,
607
+ updatedAt: Date.now(),
608
+ });
601
609
  });
602
610
  }
603
611
 
@@ -1,3 +1,4 @@
1
+ import { materializeSdkReference } from './sdk-reference.ts';
1
2
  import { spawn } from 'node:child_process';
2
3
  import { createHash } from 'node:crypto';
3
4
  import {
@@ -28,7 +29,10 @@ import {
28
29
  moduleReviewRevision,
29
30
  recordAutoReview,
30
31
  } from './auto-review.ts';
31
- import { attachmentInstruction } from './attachments.ts';
32
+ import {
33
+ attachmentInstruction,
34
+ materializeAttachments,
35
+ } from './attachments.ts';
32
36
  import { captureCheckpoint } from './checkpoints.ts';
33
37
  import { guardAgentPaths } from './path-guard.ts';
34
38
  import type { SandboxConfiguration } from './config.ts';
@@ -535,7 +539,7 @@ export async function* runTurn(
535
539
  context: TurnContext,
536
540
  input: TurnInput,
537
541
  ): AsyncGenerator<ChatEntry, TurnOutcome> {
538
- const session = await readSession(context.workspaceRoot, input.sessionId);
542
+ let session = await readSession(context.workspaceRoot, input.sessionId);
539
543
  const paths = sessionPaths(
540
544
  context.workspaceRoot,
541
545
  session.id,
@@ -576,6 +580,14 @@ export async function* runTurn(
576
580
  const driverId = input.driver ?? session.driver;
577
581
  const driver = await context.registry.resolve(driverId);
578
582
 
583
+ session = {
584
+ ...session,
585
+ attachments: await materializeAttachments(context.workspaceRoot, session),
586
+ };
587
+ const hasSdk = await materializeSdkReference(
588
+ context.workspaceRoot,
589
+ paths.workspace,
590
+ );
579
591
  const attachmentNote = attachmentInstruction(session.attachments);
580
592
 
581
593
  yield await appendChatEntry(context.workspaceRoot, session, {
@@ -671,6 +683,11 @@ export async function* runTurn(
671
683
  )}. This turn is yours in modules/${active.directory} only; another specialist takes the turn for the others. Each module is a project of this pnpm workspace, so a draft that imports another draft resolves the session copy.`,
672
684
  ]
673
685
  : []),
686
+ ...(hasSdk
687
+ ? [
688
+ 'Read the actual installed SDK under reference/sdk/packages and reference/sdk/modules. Its package.json maps public exports. These are readable copies inside the workspace; do not follow external SDK symlinks. Search only the API needed for the current task.',
689
+ ]
690
+ : []),
674
691
  'reference/ is read-only. Consult only the code and references needed for this task; do not preload its catalog.',
675
692
  'Other module.json files under modules/ describe the dependency graph. Only the draft module directories have sources you may change.',
676
693
  ...(active.kind === 'edit'
@@ -685,6 +702,7 @@ export async function* runTurn(
685
702
  driverId === 'claude-code' ? 'CLAUDE.md' : 'AGENTS.md',
686
703
  role.name,
687
704
  skill,
705
+ hasSdk,
688
706
  );
689
707
 
690
708
  const history = historyFrom(await readChat(context.workspaceRoot, session));
package/src/styles.css CHANGED
@@ -683,13 +683,16 @@
683
683
  color: var(--danger);
684
684
  }
685
685
 
686
- .chat__auto {
686
+ .chat-settings {
687
687
  display: inline-flex;
688
- align-items: center;
689
- gap: 6px;
690
- font-size: var(--text-sm);
691
- color: var(--ink-3);
692
- white-space: nowrap;
688
+ }
689
+
690
+ .chat-settings__panel {
691
+ position: fixed;
692
+ z-index: 20;
693
+ width: min(320px, calc(100vw - 32px));
694
+ max-height: calc(100dvh - 32px);
695
+ overflow-y: auto;
693
696
  }
694
697
 
695
698
  .chat__selection {
@@ -1571,3 +1574,102 @@
1571
1574
  font-size: var(--text-md);
1572
1575
  color: var(--ink-3);
1573
1576
  }
1577
+
1578
+ @keyframes chat-activity-spin {
1579
+ to {
1580
+ transform: rotate(360deg);
1581
+ }
1582
+ }
1583
+ .chat__event--active > svg {
1584
+ animation: chat-activity-spin 1.5s linear infinite;
1585
+ }
1586
+ @media (prefers-reduced-motion: reduce) {
1587
+ .chat__event--active > svg {
1588
+ animation: none;
1589
+ }
1590
+ }
1591
+
1592
+ /* Compact operation summaries with the original details available on demand. */
1593
+ .chat-tool {
1594
+ min-width: 0;
1595
+ color: var(--ink-2);
1596
+ font-size: var(--text-sm);
1597
+ }
1598
+ .chat-tool__summary {
1599
+ display: flex;
1600
+ align-items: center;
1601
+ gap: 8px;
1602
+ min-width: 0;
1603
+ padding: 6px 0;
1604
+ cursor: pointer;
1605
+ list-style: none;
1606
+ }
1607
+ .chat-tool__summary::-webkit-details-marker {
1608
+ display: none;
1609
+ }
1610
+ .chat-tool__summary:focus-visible {
1611
+ outline: 2px solid var(--focus);
1612
+ outline-offset: 2px;
1613
+ }
1614
+ .chat-tool__summary > svg {
1615
+ flex-shrink: 0;
1616
+ }
1617
+ .chat-tool__action {
1618
+ flex-shrink: 0;
1619
+ }
1620
+ .chat-tool__target {
1621
+ min-width: 0;
1622
+ flex: 1;
1623
+ overflow: hidden;
1624
+ text-overflow: ellipsis;
1625
+ white-space: nowrap;
1626
+ font-family: var(--font-mono);
1627
+ }
1628
+ .chat-tool__status,
1629
+ .chat-tool__duration {
1630
+ color: var(--ink-3);
1631
+ flex-shrink: 0;
1632
+ font-variant-numeric: tabular-nums;
1633
+ }
1634
+ .chat-tool--failed .chat-tool__status,
1635
+ .chat-tool--failed .chat-tool__summary > svg:first-child {
1636
+ color: var(--danger);
1637
+ }
1638
+ .chat-tool[open] .chat-tool__summary > svg:last-child {
1639
+ transform: rotate(180deg);
1640
+ }
1641
+ .chat-tool__details {
1642
+ margin: 0 0 8px 22px;
1643
+ padding: 12px;
1644
+ border: 1px solid var(--line);
1645
+ border-radius: var(--r-sm);
1646
+ }
1647
+ .chat-tool__details dt {
1648
+ color: var(--ink-3);
1649
+ margin-bottom: 4px;
1650
+ }
1651
+ .chat-tool__details dd {
1652
+ margin: 0 0 8px;
1653
+ white-space: pre-wrap;
1654
+ overflow-wrap: anywhere;
1655
+ font-family: var(--font-mono);
1656
+ }
1657
+ .chat-tool__details dd:last-child {
1658
+ margin-bottom: 0;
1659
+ }
1660
+ .chat-tool--running .chat-tool__summary > svg:first-child {
1661
+ animation: chat-activity-spin 1.5s linear infinite;
1662
+ }
1663
+ @media (prefers-reduced-motion: reduce) {
1664
+ .chat-tool--running .chat-tool__summary > svg:first-child {
1665
+ animation: none;
1666
+ }
1667
+ }
1668
+ @media (max-width: 600px) {
1669
+ .chat-tool__summary {
1670
+ flex-wrap: wrap;
1671
+ }
1672
+ .chat-tool__target {
1673
+ flex-basis: calc(100% - 32px);
1674
+ }
1675
+ }
package/vite.config.ts CHANGED
@@ -9,6 +9,7 @@ import { defineConfig } from 'vite';
9
9
  import { sandboxDirectory } from './src/server/config.ts';
10
10
  import { findFlowdularWorkspace } from './src/server/workspace-root.ts';
11
11
  import { resolvePreviewModules } from './src/server/preview-modules.ts';
12
+ import { isolatePreviewHotUpdates } from './src/server/preview-hot-updates.ts';
12
13
  import type { SandboxSession } from './src/server/sessions.ts';
13
14
 
14
15
  Object.assign(process.env, flowdularEnvironment(process.env));
@@ -103,7 +104,10 @@ function previewModules(workspaceRoot: string): Plugin {
103
104
 
104
105
  const config = {
105
106
  root: appRoot,
106
- plugins: [previewModules(workspace.root), octane()],
107
+ plugins: [
108
+ previewModules(workspace.root),
109
+ ...isolatePreviewHotUpdates(octane(), workspace.root),
110
+ ],
107
111
  resolve: {
108
112
  /* Draft module code is loaded from a session workspace outside this app.
109
113
  It resolves the workspace packages through the node_modules link the