@flowdular/sandbox 0.3.1 → 0.4.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 (72) hide show
  1. package/internal/coding-agent/src/drivers/byok.ts +16 -1
  2. package/internal/coding-agent/src/index.ts +1 -0
  3. package/internal/coding-agent/src/roles/defaults.ts +9 -0
  4. package/internal/coding-agent/src/roles/skills.ts +1 -0
  5. package/internal/coding-agent/src/types.ts +11 -0
  6. package/package.json +2 -2
  7. package/public/og.png +0 -0
  8. package/src/App.tsrx +77 -56
  9. package/src/client/DiffPane.tsrx +3 -7
  10. package/src/client/EjectModal.tsrx +7 -9
  11. package/src/client/GitHubSettingsModal.tsrx +8 -8
  12. package/src/client/ModelSettingsModal.tsrx +5 -5
  13. package/src/client/NewSessionForm.tsrx +1 -1
  14. package/src/client/PendingQuestionsCard.tsrx +1 -1
  15. package/src/client/SessionPicker.tsrx +49 -36
  16. package/src/client/SpecEditor.tsrx +1 -1
  17. package/src/client/locales/en.json +30 -16
  18. package/src/client/locales/pl.json +78 -28
  19. package/src/server/delivery/configuration.ts +16 -0
  20. package/src/server/delivery/git-pr.ts +51 -22
  21. package/src/server/delivery/types.ts +2 -0
  22. package/src/server/gates.ts +34 -8
  23. package/src/server/preview-modules.ts +43 -0
  24. package/src/server/preview-revision.ts +1 -0
  25. package/src/server/preview-runtime.ts +41 -4
  26. package/src/server/preview-seed.ts +115 -0
  27. package/src/server/recorded-adapters.ts +132 -0
  28. package/src/server/sample-data.ts +403 -0
  29. package/src/server/turns.ts +16 -1
  30. package/dist/client/assets/App-ClMOrpyg.js +0 -11
  31. package/dist/client/assets/PreviewHost-BHiHrfbQ.js +0 -1
  32. package/dist/client/assets/auto-buGEq6de.js +0 -3
  33. package/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  34. package/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  35. package/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  36. package/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  37. package/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  38. package/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  39. package/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  40. package/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  41. package/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  42. package/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  43. package/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  44. package/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  45. package/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  46. package/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  47. package/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  48. package/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  49. package/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  50. package/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  51. package/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  52. package/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  53. package/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  54. package/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  55. package/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  56. package/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  57. package/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  58. package/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  59. package/dist/client/assets/index-CvwhlQ87.js +0 -4
  60. package/dist/client/assets/styles-BPWaGf55.js +0 -3
  61. package/dist/client/assets/styles-Dx1_KOag.css +0 -1
  62. package/dist/client/favicon.svg +0 -1
  63. package/dist/server/assets/auto-CVGNYyb3.js +0 -12785
  64. package/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +0 -4125
  65. package/dist/server/assets/nodefs-DVSI7kG0.js +0 -24
  66. package/dist/server/assets/opfs-ahp-D56iokt1.js +0 -388
  67. package/dist/server/assets/token-Bvaq7Anz.js +0 -55
  68. package/dist/server/assets/token-util-BqSvdIPQ.js +0 -3
  69. package/dist/server/assets/token-util-CktFFT4z.js +0 -494
  70. package/dist/server/entry.js +0 -91671
  71. package/dist/server/favicon.svg +0 -1
  72. package/dist/server/index.html +0 -161
@@ -26,6 +26,7 @@ import {
26
26
  type CodingAgentAvailability,
27
27
  type CodingAgentDriver,
28
28
  type CodingAgentEvent,
29
+ type CodingAgentTool,
29
30
  type CodingAgentTurnRequest,
30
31
  type FileChangeKind,
31
32
  } from '../types.ts';
@@ -84,6 +85,7 @@ export function createByokDriver(
84
85
  workspacePath: string,
85
86
  allowedPaths: readonly string[],
86
87
  emit: (event: CodingAgentEvent) => void,
88
+ lent: readonly CodingAgentTool[] = [],
87
89
  ): ToolSet {
88
90
  let nextCall = 0;
89
91
  const record = (
@@ -128,7 +130,7 @@ export function createByokDriver(
128
130
  }
129
131
  };
130
132
 
131
- return {
133
+ const files: ToolSet = {
132
134
  list_files: tool({
133
135
  description:
134
136
  'List the files of the session workspace, relative to its root.',
@@ -233,6 +235,18 @@ export function createByokDriver(
233
235
  }),
234
236
  }),
235
237
  };
238
+ for (const entry of lent) {
239
+ if (entry.name in files) continue;
240
+ files[entry.name] = tool({
241
+ description: entry.description,
242
+ inputSchema: jsonSchema<Record<string, unknown>>(entry.inputSchema),
243
+ execute: (input) =>
244
+ record(entry.name, JSON.stringify(input).slice(0, 200), () =>
245
+ entry.execute(input),
246
+ ),
247
+ });
248
+ }
249
+ return files;
236
250
  }
237
251
 
238
252
  return {
@@ -299,6 +313,7 @@ export function createByokDriver(
299
313
  supplies an explicit allowlist for its selected specialist. */
300
314
  request.allowedPaths ?? [],
301
315
  emit,
316
+ request.tools,
302
317
  ),
303
318
  });
304
319
 
@@ -6,6 +6,7 @@ export {
6
6
  type CodingAgentDriverKind,
7
7
  type CodingAgentEvent,
8
8
  type CodingAgentMessage,
9
+ type CodingAgentTool,
9
10
  type CodingAgentTurnRequest,
10
11
  type FileChangeKind,
11
12
  type SandboxRuntimeMode,
@@ -62,6 +62,13 @@ If the requested surface needs a missing endpoint/service, hand off to backend.
62
62
  'src/index.ts',
63
63
  'migrations/**',
64
64
  'tests/**',
65
+ 'preview/**',
66
+ 'src/preview.ts',
67
+ 'src/research.ts',
68
+ 'src/adapters/**',
69
+ 'research-fixtures.json',
70
+ 'adapters/**',
71
+ 'templates/**',
65
72
  'module.json',
66
73
  'package.json',
67
74
  ],
@@ -75,6 +82,8 @@ Permission constants equal the approved spec. All tenant reads and writes use te
75
82
 
76
83
  Test observable behavior: successful operations, validation bounds, 401/403, uniqueness, replay and two-tenant isolation. Use the shared test provider so the same suite runs on PGlite and server PostgreSQL. Never use an owner connection to bypass a failing runtime test.
77
84
 
85
+ Operator sample data comes from the sample-data tool or reference/sample-data.json. Derive tests/fixtures/*.json and preview/seed.json from its shape with invented names, contacts and identifiers. src/preview.ts exports an idempotent seed({ tenantId, accountId, data, databases }) that writes through the module repository. A spec research section reads research-fixtures.json and each adapter its adapters/<id>.recorded.json; never declare a live adapter.
86
+
78
87
  Leave client files to the frontend engineer and tools/business-agent definitions to the agentic engineer. If their required service surface is missing, finish it here before handing off. Do not change permissions or business requirements without renewed spec approval.`,
79
88
  },
80
89
  {
@@ -34,6 +34,7 @@ export const ROLE_SKILLS: Readonly<Record<string, readonly string[]>> = {
34
34
  'test-hardening',
35
35
  'auth-security-review',
36
36
  'cli-extension',
37
+ 'integration-adapter',
37
38
  'perf-audit',
38
39
  ],
39
40
  'frontend-engineer': [
@@ -54,6 +54,16 @@ export interface CodingAgentMessage {
54
54
  readonly text: string;
55
55
  }
56
56
 
57
+ /* A read-only tool the orchestrator lends one turn. Drivers with a tool channel
58
+ offer it to the model; the local CLI drivers have none and ignore it, so the
59
+ orchestrator also leaves its answer under reference/. */
60
+ export interface CodingAgentTool {
61
+ readonly name: string;
62
+ readonly description: string;
63
+ readonly inputSchema: Readonly<Record<string, unknown>>;
64
+ execute(input: Readonly<Record<string, unknown>>): Promise<string>;
65
+ }
66
+
57
67
  export interface CodingAgentTurnRequest {
58
68
  /* The only directory the driver may read from or write to. */
59
69
  readonly workspacePath: string;
@@ -69,6 +79,7 @@ export interface CodingAgentTurnRequest {
69
79
  and resume by identifier; the BYOK driver replays it. */
70
80
  readonly history?: readonly CodingAgentMessage[] | undefined;
71
81
  readonly model?: string | null;
82
+ readonly tools?: readonly CodingAgentTool[] | undefined;
72
83
  readonly signal?: AbortSignal | undefined;
73
84
  readonly timeoutMs?: number | undefined;
74
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowdular/sandbox",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "The Flowdular sandbox: chat a change, build it behind the gates, preview it in the real application, deliver it as code.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/flowdular/flowdular/tree/main/packages/sandbox#readme",
@@ -57,7 +57,7 @@
57
57
  "octane": "0.1.51",
58
58
  "segment-state": "0.2.1",
59
59
  "vite": "8.2.2",
60
- "@flowdular/sdk": "0.3.1"
60
+ "@flowdular/sdk": "0.4.0"
61
61
  },
62
62
  "imports": {
63
63
  "#coding-agent": "./internal/coding-agent/src/index.ts"
package/public/og.png CHANGED
Binary file
package/src/App.tsrx CHANGED
@@ -2,7 +2,15 @@ import { useEffect, useMemo, useState } from 'octane';
2
2
  import { ModelSettingsModal } from './client/ModelSettingsModal.tsrx';
3
3
  import type { ConfigurationPatch } from './client/api.ts';
4
4
  import { Head, Seo } from '@octanejs/seo';
5
- import { Alert, BrandMark, Button, Icon, Tag } from '@flowdular/sdk/ui';
5
+ import {
6
+ Alert,
7
+ BrandMark,
8
+ Button,
9
+ Icon,
10
+ Tag,
11
+ TableLocaleContext,
12
+ type TableLocale,
13
+ } from '@flowdular/sdk/ui';
6
14
  import { useValue } from 'segment-state';
7
15
  import type { RenderRouteProps } from '@octanejs/vite-plugin';
8
16
  import type {
@@ -92,7 +100,18 @@ function startingPoint(
92
100
  }
93
101
 
94
102
  export function App(props?: RenderRouteProps) @{
95
- const { t } = useTranslation();
103
+ const { locale, t } = useTranslation();
104
+ const tableLocale = useMemo<TableLocale>(
105
+ () => ({
106
+ locale,
107
+ expand: t('shell.table.expand'),
108
+ collapse: t('shell.table.collapse'),
109
+ moreActions: t('shell.table.moreActions'),
110
+ copy: t('shell.table.copy'),
111
+ copied: t('shell.table.copied'),
112
+ }),
113
+ [locale],
114
+ );
96
115
  const sandbox = useMemo(() => createSandboxClientState(), []);
97
116
  const [state] = useValue(sandbox.state.state);
98
117
  const [view] = useValue(sandbox.state.view);
@@ -826,60 +845,62 @@ export function App(props?: RenderRouteProps) @{
826
845
 
827
846
  @if (!activeSessionId || !view) {
828
847
  <div class="sandbox__scroll">
829
- <SessionPicker
830
- onDeliverySettings={() => {
831
- setError('');
832
- setGitHubSettingsOpen(true);
833
- }}
834
- dashboard={state.dashboard}
835
- onRefresh={() => void refreshState().catch(
836
- (error) => setError(
837
- message(error, t('sandbox.app.error.state')),
838
- ),
839
- )}
840
- onReject={(id) => void sessionAction(
841
- id,
842
- () => rejectSandboxSession(id),
843
- t('sandbox.dashboard.rejectError'),
844
- )}
845
- sessions={state.sessions}
846
- running={state.running}
847
- roles={state.roles}
848
- drivers={state.drivers}
849
- driver={driver || state.configuration.driver}
850
- busy={busy}
851
- openMenu={sessionMenu}
852
- confirmDelete={confirmDelete}
853
- showArchived={showArchived}
854
- onDriver={setDriver}
855
- onOpen={(id) => void openSession(id)}
856
- onCreate={(brief, chosen, attachments) => void startSession(
857
- brief,
858
- chosen,
859
- attachments,
860
- )}
861
- onMenu={(id) => {
862
- setSessionMenu(id);
863
- setConfirmDelete('');
864
- }}
865
- onShowArchived={setShowArchived}
866
- onArchive={(id) => void sessionAction(
867
- id,
868
- () => archiveSandboxSession(id),
869
- t('sandbox.app.error.archiveSession'),
870
- )}
871
- onRestore={(id) => void sessionAction(
872
- id,
873
- () => restoreSandboxSession(id),
874
- t('sandbox.app.error.restoreSession'),
875
- )}
876
- onConfirmDelete={setConfirmDelete}
877
- onDelete={(id) => void sessionAction(
878
- id,
879
- () => deleteSandboxSession(id),
880
- t('sandbox.app.error.deleteSession'),
881
- )}
882
- />
848
+ <TableLocaleContext.Provider value={tableLocale}>
849
+ <SessionPicker
850
+ onDeliverySettings={() => {
851
+ setError('');
852
+ setGitHubSettingsOpen(true);
853
+ }}
854
+ dashboard={state.dashboard}
855
+ onRefresh={() => void refreshState().catch(
856
+ (error) => setError(
857
+ message(error, t('sandbox.app.error.state')),
858
+ ),
859
+ )}
860
+ onReject={(id) => void sessionAction(
861
+ id,
862
+ () => rejectSandboxSession(id),
863
+ t('sandbox.dashboard.rejectError'),
864
+ )}
865
+ sessions={state.sessions}
866
+ running={state.running}
867
+ roles={state.roles}
868
+ drivers={state.drivers}
869
+ driver={driver || state.configuration.driver}
870
+ busy={busy}
871
+ openMenu={sessionMenu}
872
+ confirmDelete={confirmDelete}
873
+ showArchived={showArchived}
874
+ onDriver={setDriver}
875
+ onOpen={(id) => void openSession(id)}
876
+ onCreate={(brief, chosen, attachments) => void startSession(
877
+ brief,
878
+ chosen,
879
+ attachments,
880
+ )}
881
+ onMenu={(id) => {
882
+ setSessionMenu(id);
883
+ setConfirmDelete('');
884
+ }}
885
+ onShowArchived={setShowArchived}
886
+ onArchive={(id) => void sessionAction(
887
+ id,
888
+ () => archiveSandboxSession(id),
889
+ t('sandbox.app.error.archiveSession'),
890
+ )}
891
+ onRestore={(id) => void sessionAction(
892
+ id,
893
+ () => restoreSandboxSession(id),
894
+ t('sandbox.app.error.restoreSession'),
895
+ )}
896
+ onConfirmDelete={setConfirmDelete}
897
+ onDelete={(id) => void sessionAction(
898
+ id,
899
+ () => deleteSandboxSession(id),
900
+ t('sandbox.app.error.deleteSession'),
901
+ )}
902
+ />
903
+ </TableLocaleContext.Provider>
883
904
  </div>
884
905
  } @else {
885
906
  <>
@@ -76,13 +76,9 @@ export function DiffPane(props: DiffPaneProps) @{
76
76
  @if (groups.length > 1) {
77
77
  <header class="diff__module">
78
78
  <span class="ui-mono">{'modules/' + group.module}</span>
79
- <span class="ui-help">
80
- {group.diffs.length === 1
81
- ? t('sandbox.diff.file.one')
82
- : t('sandbox.diff.file.other', {
83
- count: group.diffs.length,
84
- })}
85
- </span>
79
+ <span class="ui-help">{t('sandbox.diff.file', {
80
+ count: group.diffs.length,
81
+ })}</span>
86
82
  </header>
87
83
  }
88
84
  @for (const diff of group.diffs; key fileKey(diff)) {
@@ -134,10 +134,9 @@ function newPackagesOf(plan: EjectPlanView): readonly string[] {
134
134
 
135
135
  /* What landed, named: one module by its id, several by their ids. */
136
136
  function delivered(summary: EjectSummary): string {
137
- return `${summary.modules.join(', ')} ${t(
138
- 'sandbox.eject.deliveredVerb.' +
139
- (summary.modules.length === 1 ? 'one' : 'other'),
140
- )}`;
137
+ return `${summary.modules.join(', ')} ${t('sandbox.eject.deliveredVerb', {
138
+ count: summary.modules.length,
139
+ })}`;
141
140
  }
142
141
 
143
142
  function deliveredAt(at: number): string {
@@ -174,11 +173,10 @@ export function EjectModal(props: EjectModalProps) @{
174
173
  }
175
174
  {phase === 'done'
176
175
  ? t('sandbox.eject.delivered')
177
- : props.plan.modules.length === 1
178
- ? t('sandbox.eject.title.one', { module: props.plan.moduleId })
179
- : t('sandbox.eject.title.other', {
180
- count: props.plan.modules.length,
181
- })}
176
+ : t('sandbox.eject.title', {
177
+ module: props.plan.moduleId,
178
+ count: props.plan.modules.length,
179
+ })}
182
180
  </span>
183
181
  <span class="sandbox__spacer"></span>
184
182
  <Tag mono>
@@ -85,7 +85,7 @@ export function GitHubSettingsModal(props: GitHubSettingsModalProps) @{
85
85
  name="githubRepository"
86
86
  defaultValue={github.repository ?? ''}
87
87
  placeholder={t('sandbox.github.repository.placeholder')}
88
- maxlength={200}
88
+ maxLength={200}
89
89
  />
90
90
  </FormField>
91
91
  <FormField
@@ -112,7 +112,7 @@ export function GitHubSettingsModal(props: GitHubSettingsModalProps) @{
112
112
  class="ui-input ui-mono"
113
113
  name="githubReviewers"
114
114
  defaultValue={github.reviewers.join(', ')}
115
- maxlength={1000}
115
+ maxLength={1000}
116
116
  />
117
117
  </FormField>
118
118
  <details>
@@ -129,7 +129,7 @@ export function GitHubSettingsModal(props: GitHubSettingsModalProps) @{
129
129
  class="ui-input ui-mono"
130
130
  name="githubRemote"
131
131
  defaultValue={github.remote}
132
- maxlength={120}
132
+ maxLength={120}
133
133
  />
134
134
  </FormField>
135
135
  <FormField
@@ -140,7 +140,7 @@ export function GitHubSettingsModal(props: GitHubSettingsModalProps) @{
140
140
  class="ui-input ui-mono"
141
141
  name="githubBaseBranch"
142
142
  defaultValue={github.baseBranch}
143
- maxlength={120}
143
+ maxLength={120}
144
144
  />
145
145
  </FormField>
146
146
  </div>
@@ -150,7 +150,7 @@ export function GitHubSettingsModal(props: GitHubSettingsModalProps) @{
150
150
  class="ui-input ui-mono"
151
151
  name="githubBranchPrefix"
152
152
  defaultValue={github.branchPrefix}
153
- maxlength={120}
153
+ maxLength={120}
154
154
  />
155
155
  </FormField>
156
156
  <FormField
@@ -161,7 +161,7 @@ export function GitHubSettingsModal(props: GitHubSettingsModalProps) @{
161
161
  class="ui-input ui-mono"
162
162
  name="githubForkOwner"
163
163
  defaultValue={github.forkOwner ?? ''}
164
- maxlength={120}
164
+ maxLength={120}
165
165
  />
166
166
  </FormField>
167
167
  </div>
@@ -186,9 +186,9 @@ export function GitHubSettingsModal(props: GitHubSettingsModalProps) @{
186
186
  class="ui-input ui-mono"
187
187
  name="githubToken"
188
188
  type="password"
189
- autocomplete="new-password"
189
+ autoComplete="new-password"
190
190
  placeholder={t('sandbox.github.token.placeholder')}
191
- maxlength={16384}
191
+ maxLength={16384}
192
192
  />
193
193
  </FormField>
194
194
  @if (github.tokenFingerprint) {
@@ -71,7 +71,7 @@ export function ModelSettingsModal(props: {
71
71
  class="ui-input"
72
72
  name="model"
73
73
  required
74
- maxlength={160}
74
+ maxLength={160}
75
75
  defaultValue={saved?.model ?? ''}
76
76
  />
77
77
  </FormField>
@@ -85,8 +85,8 @@ export function ModelSettingsModal(props: {
85
85
  class="ui-input"
86
86
  name="credential"
87
87
  type="password"
88
- autocomplete="new-password"
89
- maxlength={16384}
88
+ autoComplete="new-password"
89
+ maxLength={16384}
90
90
  />
91
91
  </FormField>
92
92
  <FormField label={t('sandbox.models.url')}>
@@ -95,7 +95,7 @@ export function ModelSettingsModal(props: {
95
95
  name="baseURL"
96
96
  type="url"
97
97
  required={kind === 'openai-compatible'}
98
- maxlength={2048}
98
+ maxLength={2048}
99
99
  defaultValue={saved?.baseURL ?? ''}
100
100
  />
101
101
  </FormField>
@@ -105,7 +105,7 @@ export function ModelSettingsModal(props: {
105
105
  class="ui-input"
106
106
  name="resourceName"
107
107
  required
108
- maxlength={160}
108
+ maxLength={160}
109
109
  defaultValue={saved?.resourceName ?? ''}
110
110
  />
111
111
  </FormField>
@@ -156,7 +156,7 @@ export function NewSessionForm(props: NewSessionFormProps) @{
156
156
  class="ui-textarea"
157
157
  aria-label={t('sandbox.sessions.brief.placeholder')}
158
158
  minlength={8}
159
- maxlength={20000}
159
+ maxLength={20000}
160
160
  name="brief"
161
161
  rows={4}
162
162
  placeholder={t('sandbox.sessions.brief.placeholder')}
@@ -140,7 +140,7 @@ export function PendingQuestionsCard(props: PendingQuestionsCardProps) @{
140
140
  <input
141
141
  class="ui-input"
142
142
  type="text"
143
- maxlength={MAX_ANSWER_LENGTH}
143
+ maxLength={MAX_ANSWER_LENGTH}
144
144
  aria-label={t('sandbox.questions.freeText')}
145
145
  placeholder={t('sandbox.questions.freeTextPlaceholder')}
146
146
  value={draft.typed[question.id] ?? ''}
@@ -10,6 +10,10 @@ import {
10
10
  Kpi,
11
11
  PageHeader,
12
12
  SearchField,
13
+ CellStack,
14
+ CellTag,
15
+ CellText,
16
+ CellTime,
13
17
  TableCard,
14
18
  Tag,
15
19
  type TableColumn,
@@ -80,56 +84,56 @@ function columns(): readonly TableColumn<DashboardRow>[] {
80
84
  {
81
85
  key: 'work',
82
86
  header: t('sandbox.dashboard.work'),
83
- width: '32%',
84
- cell: (row) => <span class="ui-cell">
85
- <b>{row.session.title}</b>
86
- <small>
87
- {t(
87
+ width: 'auto',
88
+ cell: (row) => <CellStack
89
+ primary={row.session.title}
90
+ secondary={[
91
+ t(
88
92
  row.session.kind === 'new-module'
89
93
  ? 'sandbox.sessions.kind.new'
90
94
  : 'sandbox.sessions.kind.change',
91
- )}
92
- ·
93
- {t('sandbox.dashboard.modules', {
95
+ ),
96
+ t('sandbox.dashboard.modules', {
94
97
  count: row.session.modules.length,
95
- })}
96
- </small>
97
- {row.personal
98
- ? null
99
- : <small>{t('sandbox.dashboard.legacyRow')}</small>}
100
- </span>,
98
+ }),
99
+ ...(row.personal ? [] : [t('sandbox.dashboard.legacyRow')]),
100
+ ].join(' · ')}
101
+ />,
101
102
  },
102
103
  {
103
104
  key: 'status',
104
105
  header: t('sandbox.dashboard.status'),
105
- width: '20%',
106
+ width: '160px',
106
107
  cell: (row) =>
107
- <Tag tone={tone(row.status)} dot>{t(
108
- 'sandbox.dashboard.status.' + row.status,
109
- )}</Tag>,
108
+ <CellTag
109
+ tone={tone(row.status)}
110
+ dot
111
+ label={t('sandbox.dashboard.status.' + row.status)}
112
+ />,
110
113
  },
111
114
  {
112
115
  key: 'tokens',
113
116
  header: t('sandbox.dashboard.table.tokens'),
114
- width: '13%',
117
+ width: '120px',
115
118
  numeric: true,
116
- cell: (row) => formatTokens(row.usage, activeLocale()),
119
+ priority: 2,
120
+ cell: (row) =>
121
+ <CellText value={formatTokens(row.usage, activeLocale())} />,
117
122
  },
118
123
  {
119
124
  key: 'cost',
120
125
  header: t('sandbox.dashboard.table.cost'),
121
- width: '13%',
126
+ width: '120px',
122
127
  numeric: true,
123
- cell: (row) => formatCost(row.usage, activeLocale()),
128
+ priority: 2,
129
+ cell: (row) => <CellText value={formatCost(row.usage, activeLocale())} />,
124
130
  },
125
131
  {
126
132
  key: 'updated',
127
133
  header: t('sandbox.dashboard.table.updated'),
128
- width: '12%',
129
- cell: (row) => new Intl.DateTimeFormat(activeLocale(), {
130
- day: 'numeric',
131
- month: 'short',
132
- }).format(row.session.updatedAt),
134
+ width: '120px',
135
+ priority: 2,
136
+ cell: (row) => <CellTime value={row.session.updatedAt} kind="date" />,
133
137
  },
134
138
  ];
135
139
  }
@@ -381,25 +385,34 @@ export function SessionPicker(props: SessionPickerProps) @{
381
385
  {
382
386
  key: 'phase',
383
387
  header: t('sandbox.dashboard.phase'),
384
- width: '50%',
385
- cell: (phase) => t('sandbox.dashboard.role.' + phase.role) ===
386
- 'sandbox.dashboard.role.' + phase.role
387
- ? t('sandbox.dashboard.role.unknown')
388
- : t('sandbox.dashboard.role.' + phase.role),
388
+ width: 'auto',
389
+ cell: (phase) =>
390
+ <CellText
391
+ value={t('sandbox.dashboard.role.' + phase.role) ===
392
+ 'sandbox.dashboard.role.' + phase.role
393
+ ? t('sandbox.dashboard.role.unknown')
394
+ : t('sandbox.dashboard.role.' + phase.role)}
395
+ />,
389
396
  },
390
397
  {
391
398
  key: 'tokens',
392
399
  header: t('sandbox.dashboard.table.tokens'),
393
- width: '25%',
400
+ width: '140px',
394
401
  numeric: true,
395
- cell: (phase) => formatTokens(phase.usage, activeLocale()),
402
+ cell: (phase) =>
403
+ <CellText
404
+ value={formatTokens(phase.usage, activeLocale())}
405
+ />,
396
406
  },
397
407
  {
398
408
  key: 'cost',
399
409
  header: t('sandbox.dashboard.table.cost'),
400
- width: '25%',
410
+ width: '140px',
401
411
  numeric: true,
402
- cell: (phase) => formatCost(phase.usage, activeLocale()),
412
+ cell: (phase) =>
413
+ <CellText
414
+ value={formatCost(phase.usage, activeLocale())}
415
+ />,
403
416
  },
404
417
  ]}
405
418
  empty={{
@@ -97,7 +97,7 @@ export function SpecEditor(props: SpecEditorProps) @{
97
97
  <textarea
98
98
  class="ui-textarea spec-editor__text"
99
99
  aria-label={t('sandbox.workbench.specification')}
100
- spellcheck={false}
100
+ spellCheck={false}
101
101
  disabled={!loaded || saving}
102
102
  value={text}
103
103
  onInput={(event) => {