@tt-a1i/openpi 0.6.0 → 0.7.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 (60) hide show
  1. package/README.md +13 -11
  2. package/SETUP.md +2 -2
  3. package/THIRD_PARTY_NOTICES.md +242 -0
  4. package/extensions/ai-providers/README.md +12 -6
  5. package/extensions/ai-providers/cursor/connect-frame-reader.ts +76 -0
  6. package/extensions/ai-providers/cursor/input-images.ts +2 -3
  7. package/extensions/ai-providers/cursor/proto.ts +218 -11
  8. package/extensions/ai-providers/cursor/protobuf.ts +12 -2
  9. package/extensions/ai-providers/cursor/provider.ts +280 -37
  10. package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
  11. package/extensions/ai-providers/index.ts +3 -3
  12. package/extensions/file-mutation-display/index.ts +13 -9
  13. package/extensions/shared/agent-transcript.ts +3 -2
  14. package/extensions/shared/child-session.ts +14 -0
  15. package/extensions/subagents/index.ts +20 -3
  16. package/extensions/subagents/src/agent-types.ts +5 -17
  17. package/extensions/subagents/src/backends/pi.ts +70 -59
  18. package/extensions/subagents/src/backends/tool-preview.ts +29 -0
  19. package/extensions/subagents/src/manager.ts +2 -71
  20. package/extensions/subagents/src/prompt.ts +2 -2
  21. package/extensions/subagents/src/runtime.ts +10 -3
  22. package/extensions/web/index.ts +69 -5
  23. package/extensions/workflows/artifacts.ts +362 -23
  24. package/extensions/workflows/dashboard.ts +141 -21
  25. package/extensions/workflows/index.ts +62 -20
  26. package/extensions/workflows/progress-projection.ts +7 -1
  27. package/extensions/workflows/runner.ts +5 -162
  28. package/extensions/workflows/sandbox.ts +4 -0
  29. package/package.json +28 -4
  30. package/skills/subagents/REFERENCE.md +6 -7
  31. package/skills/subagents/SKILL.md +1 -1
  32. package/skills/workflows/REFERENCE.md +3 -1
  33. package/web/dist/app.js +87 -0
  34. package/web/dist/favicon.svg +9 -0
  35. package/web/dist/index.html +15 -0
  36. package/web/dist/styles.css +3 -0
  37. package/web/host/web-host.ts +6 -9
  38. package/web/ui/index.html +3 -131
  39. package/web/ui/public/favicon.svg +9 -0
  40. package/web/ui/src/app/App.tsx +134 -0
  41. package/web/ui/src/app/providers.tsx +38 -0
  42. package/web/ui/src/components/Markdown.tsx +58 -0
  43. package/web/ui/src/components/OpenPiLogo.tsx +41 -0
  44. package/web/ui/src/features/activity/ActivityBar.tsx +120 -0
  45. package/web/ui/src/features/composer/Composer.tsx +237 -0
  46. package/web/ui/src/features/sessions/SessionSidebar.tsx +418 -0
  47. package/web/ui/src/features/transcript/Transcript.tsx +860 -0
  48. package/web/ui/src/i18n.ts +159 -0
  49. package/web/ui/src/lib/format.ts +57 -0
  50. package/web/ui/src/main.tsx +16 -0
  51. package/web/ui/src/protocol/client.ts +199 -0
  52. package/web/ui/src/protocol/event-stream.ts +88 -0
  53. package/web/ui/src/store/web-store.ts +926 -0
  54. package/web/ui/src/styles.css +420 -0
  55. package/web/ui/tsconfig.json +12 -0
  56. package/web/ui/vite-env.d.ts +1 -0
  57. package/web/vite.config.mjs +21 -1
  58. package/web/host/static-assets.ts +0 -4
  59. package/web/ui/app.js +0 -1700
  60. package/web/ui/styles.css +0 -680
@@ -0,0 +1,237 @@
1
+ import { DropdownMenu } from "@astryxdesign/core/DropdownMenu";
2
+ import { Tooltip } from "@astryxdesign/core/Tooltip";
3
+ import { Check, ChevronDown, Folder, Plus, Send, Square } from "lucide-react";
4
+ import { type FormEvent, useRef, useState } from "react";
5
+ import { useTranslation } from "react-i18next";
6
+ import type { WebSnapshot } from "../../../../protocol/types.ts";
7
+ import { workspaceName } from "../../lib/format.ts";
8
+ import type { WebStoreActions, WebStoreState } from "../../store/web-store.ts";
9
+ import { ActivityBar } from "../activity/ActivityBar.tsx";
10
+
11
+ interface ComposerProps {
12
+ snapshot: WebSnapshot | null;
13
+ selectedWorkspace: string | null;
14
+ sessionSwitching: boolean;
15
+ promptAdmissionPending: boolean;
16
+ liveRunning: boolean;
17
+ landing: boolean;
18
+ actions: WebStoreActions;
19
+ activeTurn: WebStoreState["activeTurn"];
20
+ turnCancellationPending: boolean;
21
+ turnTerminalStatus: string | null;
22
+ pendingFollowUpsReceipt: number | null;
23
+ }
24
+
25
+ export function Composer(props: ComposerProps) {
26
+ const { t } = useTranslation();
27
+ const [prompt, setPrompt] = useState("");
28
+ const textarea = useRef<HTMLTextAreaElement>(null);
29
+ const selected = props.snapshot?.selectedSession;
30
+ const active = Boolean(
31
+ selected?.id && selected.id === props.snapshot?.currentSessionId,
32
+ );
33
+ const draftSession = Boolean(
34
+ props.selectedWorkspace && !selected && !props.snapshot?.currentSessionId,
35
+ );
36
+ const canCompose = active || draftSession;
37
+ const running =
38
+ props.snapshot?.runtime.status === "running" || props.liveRunning;
39
+ const canStop =
40
+ active &&
41
+ running &&
42
+ Boolean(props.activeTurn ?? props.snapshot?.runtime.activeTurn);
43
+ const disabled =
44
+ props.sessionSwitching || (!canCompose && Boolean(props.selectedWorkspace));
45
+
46
+ const resize = (element: HTMLTextAreaElement) => {
47
+ element.style.height = "auto";
48
+ element.style.height = `${Math.min(element.scrollHeight, 220)}px`;
49
+ element.style.overflowY = element.scrollHeight > 220 ? "auto" : "hidden";
50
+ };
51
+
52
+ const send = async (event?: FormEvent) => {
53
+ event?.preventDefault();
54
+ if (!props.selectedWorkspace) {
55
+ await props.actions.chooseWorkspace();
56
+ return;
57
+ }
58
+ if (await props.actions.sendPrompt(prompt)) {
59
+ setPrompt("");
60
+ if (textarea.current) {
61
+ textarea.current.style.height = "auto";
62
+ textarea.current.style.overflowY = "hidden";
63
+ }
64
+ }
65
+ };
66
+
67
+ const workspaceItems = [
68
+ ...(props.snapshot?.workspaces ?? []).map((workspace) => ({
69
+ id: workspace.path,
70
+ label: workspace.name,
71
+ icon: <Folder />,
72
+ endContent:
73
+ workspace.path === props.selectedWorkspace ? <Check /> : undefined,
74
+ onClick: () => props.actions.setWorkspace(workspace.path),
75
+ })),
76
+ { type: "divider" as const },
77
+ {
78
+ id: "add",
79
+ label: t("addWorkspaceMenu"),
80
+ icon: <Plus />,
81
+ onClick: () => void props.actions.chooseWorkspace(),
82
+ },
83
+ ];
84
+ const currentModel =
85
+ props.snapshot?.models.find((model) => model.current) ??
86
+ props.snapshot?.models[0];
87
+ const modelItems = (props.snapshot?.models ?? []).map((model) => ({
88
+ id: `${model.provider}/${model.id}`,
89
+ label: model.label,
90
+ endContent: model.current ? <Check /> : undefined,
91
+ onClick: () =>
92
+ void props.actions.selectModel(`${model.provider}/${model.id}`),
93
+ }));
94
+ const placeholder = !props.selectedWorkspace
95
+ ? t("promptStart")
96
+ : props.landing
97
+ ? t("promptTask")
98
+ : active
99
+ ? t("promptMessage")
100
+ : t("promptReadonly");
101
+ const hint = props.turnCancellationPending
102
+ ? t("stoppingTurn")
103
+ : props.turnTerminalStatus === "cancelled"
104
+ ? t("stoppedTurn")
105
+ : props.pendingFollowUpsReceipt !== null
106
+ ? props.pendingFollowUpsReceipt > 0
107
+ ? t("pendingFollowUpsHint", { count: props.pendingFollowUpsReceipt })
108
+ : t("acceptedHint")
109
+ : canCompose
110
+ ? running
111
+ ? t("queuedHint")
112
+ : t("enterHint")
113
+ : t("activeOnlyHint");
114
+
115
+ return (
116
+ <div className="composer-dock">
117
+ {active && <ActivityBar snapshot={props.snapshot} />}
118
+ {props.landing && (
119
+ <div className="workspace-picker-row">
120
+ <DropdownMenu
121
+ className="workspace-picker-menu"
122
+ button={{
123
+ label: props.selectedWorkspace
124
+ ? workspaceName(props.selectedWorkspace)
125
+ : t("selectWorkspace"),
126
+ icon: <Folder />,
127
+ size: "md",
128
+ variant: "ghost",
129
+ className: "workspace-picker",
130
+ }}
131
+ items={workspaceItems}
132
+ menuWidth={240}
133
+ placement="above"
134
+ alignment="start"
135
+ hasChevron
136
+ />
137
+ </div>
138
+ )}
139
+ <form
140
+ className={`composer ${props.selectedWorkspace ? "" : "dormant"}`}
141
+ onSubmit={(event) => void send(event)}
142
+ >
143
+ {!props.selectedWorkspace && (
144
+ <button
145
+ className="dormant-overlay"
146
+ type="button"
147
+ aria-label={t("selectWorkspace")}
148
+ onClick={() => void props.actions.chooseWorkspace()}
149
+ />
150
+ )}
151
+ <textarea
152
+ ref={textarea}
153
+ value={prompt}
154
+ rows={1}
155
+ disabled={disabled}
156
+ readOnly={!props.selectedWorkspace}
157
+ aria-label={t("describeTask")}
158
+ placeholder={placeholder}
159
+ onChange={(event) => {
160
+ setPrompt(event.target.value);
161
+ resize(event.currentTarget);
162
+ }}
163
+ onKeyDown={(event) => {
164
+ if (
165
+ event.key === "Enter" &&
166
+ !event.shiftKey &&
167
+ !event.nativeEvent.isComposing
168
+ ) {
169
+ event.preventDefault();
170
+ void send();
171
+ }
172
+ }}
173
+ />
174
+ <div className="composer-toolbar">
175
+ <div className="model-picker-wrap">
176
+ <DropdownMenu
177
+ className="model-menu"
178
+ button={{
179
+ label: currentModel?.label || t("noModels"),
180
+ endContent: <ChevronDown />,
181
+ size: "sm",
182
+ variant: "ghost",
183
+ className: "model-picker",
184
+ isDisabled:
185
+ props.sessionSwitching ||
186
+ !active ||
187
+ !props.selectedWorkspace ||
188
+ props.liveRunning ||
189
+ !modelItems.length,
190
+ }}
191
+ items={modelItems}
192
+ menuWidth={260}
193
+ placement="above"
194
+ alignment="end"
195
+ hasChevron={false}
196
+ />
197
+ </div>
198
+ {canStop ? (
199
+ <Tooltip content={t("stopTurn")} placement="above">
200
+ <button
201
+ className="send-button"
202
+ type="button"
203
+ aria-label={t("stopTurn")}
204
+ disabled={
205
+ props.turnCancellationPending || props.sessionSwitching
206
+ }
207
+ onClick={() => void props.actions.cancelActiveTurn()}
208
+ >
209
+ <Square />
210
+ </button>
211
+ </Tooltip>
212
+ ) : (
213
+ <Tooltip content={t("send")} placement="above">
214
+ <button
215
+ className="send-button"
216
+ type="submit"
217
+ aria-label={t("send")}
218
+ disabled={
219
+ props.sessionSwitching ||
220
+ !canCompose ||
221
+ !props.selectedWorkspace ||
222
+ props.promptAdmissionPending ||
223
+ !prompt.trim()
224
+ }
225
+ >
226
+ <Send />
227
+ </button>
228
+ </Tooltip>
229
+ )}
230
+ </div>
231
+ <div className="composer-hint" aria-live="polite">
232
+ {hint}
233
+ </div>
234
+ </form>
235
+ </div>
236
+ );
237
+ }
@@ -0,0 +1,418 @@
1
+ import { Dialog } from "@astryxdesign/core/Dialog";
2
+ import { DropdownMenu } from "@astryxdesign/core/DropdownMenu";
3
+ import type { DropdownMenuOption } from "@astryxdesign/core/DropdownMenu";
4
+ import { Tooltip } from "@astryxdesign/core/Tooltip";
5
+ import {
6
+ Archive,
7
+ ChevronsLeft,
8
+ MoreHorizontal,
9
+ Plus,
10
+ Search,
11
+ SquarePen,
12
+ Trash2,
13
+ X,
14
+ } from "lucide-react";
15
+ import { useEffect, useMemo, useRef, useState } from "react";
16
+ import { useTranslation } from "react-i18next";
17
+ import type { WebSnapshot } from "../../../../protocol/types.ts";
18
+ import { OpenPiLogo } from "../../components/OpenPiLogo.tsx";
19
+ import { relativeTime, sessionTitle } from "../../lib/format.ts";
20
+ import type { WebStoreActions } from "../../store/web-store.ts";
21
+
22
+ interface SessionSidebarProps {
23
+ snapshot: WebSnapshot | null;
24
+ selectedPath: string | null;
25
+ selectedWorkspace: string | null;
26
+ collapsed: Set<string>;
27
+ query: string;
28
+ searchOpen: boolean;
29
+ mobileOpen: boolean;
30
+ actions: WebStoreActions;
31
+ }
32
+
33
+ type EditTarget = { kind: "workspace" | "session"; path: string; name: string };
34
+ type DeleteTarget = { path: string; name: string };
35
+
36
+ function ActionMenu({
37
+ items,
38
+ label,
39
+ }: {
40
+ items: DropdownMenuOption[];
41
+ label: string;
42
+ }) {
43
+ return (
44
+ <span className="astryx-menu-trigger">
45
+ <DropdownMenu
46
+ button={{
47
+ label,
48
+ icon: <MoreHorizontal />,
49
+ isIconOnly: true,
50
+ size: "sm",
51
+ variant: "ghost",
52
+ className: "menu-action-button",
53
+ }}
54
+ items={items}
55
+ menuWidth={190}
56
+ placement="below"
57
+ alignment="end"
58
+ hasChevron={false}
59
+ />
60
+ </span>
61
+ );
62
+ }
63
+
64
+ export function SessionSidebar(props: SessionSidebarProps) {
65
+ const { t } = useTranslation();
66
+ const [editTarget, setEditTarget] = useState<EditTarget | null>(null);
67
+ const [deleteTarget, setDeleteTarget] = useState<DeleteTarget | null>(null);
68
+ const [draft, setDraft] = useState("");
69
+ const searchInput = useRef<HTMLInputElement>(null);
70
+ const editInput = useRef<HTMLInputElement>(null);
71
+ const snapshot = props.snapshot;
72
+
73
+ useEffect(() => {
74
+ if (props.searchOpen) searchInput.current?.focus();
75
+ }, [props.searchOpen]);
76
+ useEffect(() => {
77
+ if (!editTarget) return;
78
+ editInput.current?.focus();
79
+ editInput.current?.select();
80
+ }, [editTarget]);
81
+
82
+ const grouped = useMemo(() => {
83
+ const query = props.query.trim().toLowerCase();
84
+ const visible = (workspacePath?: string) =>
85
+ (snapshot?.sessions ?? []).filter(
86
+ (session) =>
87
+ !session.archived &&
88
+ (workspacePath === "__ungrouped__"
89
+ ? session.ungrouped
90
+ : session.cwd === workspacePath && !session.ungrouped) &&
91
+ (!query ||
92
+ `${sessionTitle(session, t("untitledSession"))} ${session.cwd}`
93
+ .toLowerCase()
94
+ .includes(query)),
95
+ );
96
+ return [
97
+ ...(snapshot?.workspaces ?? []).map((workspace) => ({
98
+ ...workspace,
99
+ sessions: visible(workspace.path),
100
+ ungrouped: false,
101
+ })),
102
+ {
103
+ path: "__ungrouped__",
104
+ name: t("ungrouped"),
105
+ current: false,
106
+ sessions: visible("__ungrouped__"),
107
+ ungrouped: true,
108
+ },
109
+ ].filter(
110
+ (group) => group.sessions.length > 0 || (!group.ungrouped && !query),
111
+ );
112
+ }, [props.query, snapshot, t]);
113
+
114
+ const openEdit = (target: EditTarget) => {
115
+ setDraft(target.name);
116
+ setEditTarget(target);
117
+ };
118
+ const saveEdit = async () => {
119
+ const name = draft.trim();
120
+ if (!editTarget || !name) return;
121
+ if (editTarget.kind === "workspace")
122
+ await props.actions.renameWorkspace(editTarget.path, name);
123
+ else await props.actions.renameSession(editTarget.path, name);
124
+ setEditTarget(null);
125
+ };
126
+
127
+ return (
128
+ <aside className="session-sidebar" aria-label="Session navigation">
129
+ <div className="sidebar-brand">
130
+ <OpenPiLogo compact />
131
+ <Tooltip content={t("collapseSidebar")} placement="end">
132
+ <button
133
+ className="collapse-button"
134
+ type="button"
135
+ aria-label={t("collapseSidebar")}
136
+ onClick={() =>
137
+ props.mobileOpen
138
+ ? props.actions.closeMobileSidebar()
139
+ : props.actions.toggleSidebar(false)
140
+ }
141
+ >
142
+ <ChevronsLeft />
143
+ </button>
144
+ </Tooltip>
145
+ </div>
146
+
147
+ <button
148
+ className="new-session-button"
149
+ type="button"
150
+ onClick={() =>
151
+ props.selectedWorkspace
152
+ ? void props.actions.createSession(props.selectedWorkspace)
153
+ : void props.actions.chooseWorkspace()
154
+ }
155
+ >
156
+ <SquarePen />
157
+ <span>{t("newSession")}</span>
158
+ </button>
159
+
160
+ <div
161
+ className={`workspace-heading ${props.searchOpen ? "is-searching" : ""}`}
162
+ >
163
+ <span className="workspace-heading-label">{t("workspaces")}</span>
164
+ <div className="session-search">
165
+ <Search />
166
+ <input
167
+ ref={searchInput}
168
+ type="search"
169
+ value={props.query}
170
+ placeholder={t("searchPlaceholder")}
171
+ aria-label={t("searchConversations")}
172
+ onChange={(event) => props.actions.setQuery(event.target.value)}
173
+ />
174
+ <button
175
+ type="button"
176
+ aria-label={t("closeSearch")}
177
+ onClick={() => props.actions.setSearchOpen(false)}
178
+ >
179
+ <X />
180
+ </button>
181
+ </div>
182
+ <div className="workspace-actions">
183
+ <Tooltip content={t("searchConversations")}>
184
+ <button
185
+ className="icon-button"
186
+ type="button"
187
+ aria-label={t("searchConversations")}
188
+ onClick={() => props.actions.setSearchOpen(true)}
189
+ >
190
+ <Search />
191
+ </button>
192
+ </Tooltip>
193
+ <Tooltip content={t("addWorkspace")}>
194
+ <button
195
+ className="icon-button"
196
+ type="button"
197
+ aria-label={t("addWorkspace")}
198
+ onClick={() => void props.actions.chooseWorkspace()}
199
+ >
200
+ <Plus />
201
+ </button>
202
+ </Tooltip>
203
+ </div>
204
+ </div>
205
+
206
+ <div className="workspace-tree">
207
+ {grouped.length ? (
208
+ grouped.map((group) => {
209
+ const collapsed = props.collapsed.has(group.path);
210
+ return (
211
+ <section
212
+ className={`workspace-group ${collapsed ? "collapsed" : ""}`}
213
+ key={group.path}
214
+ >
215
+ <div className="workspace-button">
216
+ <button
217
+ className="workspace-label"
218
+ type="button"
219
+ aria-expanded={!collapsed}
220
+ title={
221
+ group.path === "__ungrouped__" ? undefined : group.path
222
+ }
223
+ onClick={() => props.actions.toggleWorkspace(group.path)}
224
+ >
225
+ <span className="workspace-toggle" aria-hidden="true">
226
+ <span className="workspace-chevron">⌄</span>
227
+ </span>
228
+ <strong>{group.name}</strong>
229
+ </button>
230
+ {!group.ungrouped && (
231
+ <span className="workspace-row-actions">
232
+ <ActionMenu
233
+ label="Workspace options"
234
+ items={[
235
+ {
236
+ id: "rename",
237
+ label: t("renameWorkspace"),
238
+ icon: <SquarePen />,
239
+ onClick: () =>
240
+ openEdit({
241
+ kind: "workspace",
242
+ path: group.path,
243
+ name: group.name,
244
+ }),
245
+ },
246
+ {
247
+ id: "remove",
248
+ label: t("removeWorkspace"),
249
+ icon: <Trash2 />,
250
+ variant: "destructive",
251
+ onClick: () =>
252
+ setDeleteTarget({
253
+ path: group.path,
254
+ name: group.name,
255
+ }),
256
+ },
257
+ ]}
258
+ />
259
+ <Tooltip content={t("newSession")}>
260
+ <button
261
+ className="workspace-action"
262
+ type="button"
263
+ aria-label={`${t("newSession")} ${group.name}`}
264
+ onClick={(event) => {
265
+ event.stopPropagation();
266
+ void props.actions.createSession(group.path);
267
+ }}
268
+ >
269
+ <Plus />
270
+ </button>
271
+ </Tooltip>
272
+ </span>
273
+ )}
274
+ </div>
275
+ <div className="workspace-sessions">
276
+ {group.sessions.length ? (
277
+ group.sessions.map((session) => (
278
+ <div className="session-row" key={session.path}>
279
+ <button
280
+ className={`session ${session.path === props.selectedPath ? "active" : ""}`}
281
+ type="button"
282
+ aria-current={
283
+ session.path === props.selectedPath
284
+ ? "page"
285
+ : undefined
286
+ }
287
+ title={sessionTitle(session, t("untitledSession"))}
288
+ onClick={() =>
289
+ void props.actions.selectSession(session.path)
290
+ }
291
+ >
292
+ <span className="session-title">
293
+ {sessionTitle(session, t("untitledSession"))}
294
+ </span>
295
+ <span className="session-time">
296
+ {relativeTime(session.modified)}
297
+ </span>
298
+ </button>
299
+ <ActionMenu
300
+ label={t("conversationOptions")}
301
+ items={[
302
+ {
303
+ id: "rename",
304
+ label: t("renameConversation"),
305
+ icon: <SquarePen />,
306
+ onClick: () =>
307
+ openEdit({
308
+ kind: "session",
309
+ path: session.path,
310
+ name: sessionTitle(
311
+ session,
312
+ t("untitledSession"),
313
+ ),
314
+ }),
315
+ },
316
+ {
317
+ id: "archive",
318
+ label: t("archiveConversation"),
319
+ icon: <Archive />,
320
+ onClick: () =>
321
+ void props.actions.archiveSession(session.path),
322
+ },
323
+ ]}
324
+ />
325
+ </div>
326
+ ))
327
+ ) : (
328
+ <div className="empty">{t("noConversations")}</div>
329
+ )}
330
+ </div>
331
+ </section>
332
+ );
333
+ })
334
+ ) : (
335
+ <div className="empty">
336
+ {props.query ? t("noMatching") : t("noSessions")}
337
+ </div>
338
+ )}
339
+ </div>
340
+
341
+ <Dialog
342
+ isOpen={Boolean(editTarget)}
343
+ onOpenChange={(open: boolean) => !open && setEditTarget(null)}
344
+ purpose="form"
345
+ width={400}
346
+ aria-label={
347
+ editTarget?.kind === "workspace"
348
+ ? t("renameWorkspace")
349
+ : t("renameConversation")
350
+ }
351
+ >
352
+ <form
353
+ className="openpi-dialog"
354
+ onSubmit={(event) => {
355
+ event.preventDefault();
356
+ void saveEdit();
357
+ }}
358
+ >
359
+ <strong>
360
+ {editTarget?.kind === "workspace"
361
+ ? t("renameWorkspace")
362
+ : t("renameConversation")}
363
+ </strong>
364
+ <input
365
+ ref={editInput}
366
+ value={draft}
367
+ maxLength={80}
368
+ aria-label={
369
+ editTarget?.kind === "workspace"
370
+ ? t("workspaceName")
371
+ : t("conversationName")
372
+ }
373
+ onChange={(event) => setDraft(event.target.value)}
374
+ />
375
+ <div className="dialog-actions">
376
+ <button type="button" onClick={() => setEditTarget(null)}>
377
+ {t("cancel")}
378
+ </button>
379
+ <button type="submit" className="primary">
380
+ {t("save")}
381
+ </button>
382
+ </div>
383
+ </form>
384
+ </Dialog>
385
+
386
+ <Dialog
387
+ isOpen={Boolean(deleteTarget)}
388
+ onOpenChange={(open: boolean) => !open && setDeleteTarget(null)}
389
+ purpose="form"
390
+ width={440}
391
+ aria-label={t("deleteWorkspace")}
392
+ >
393
+ <div className="openpi-dialog">
394
+ <strong>{t("deleteWorkspace")}</strong>
395
+ <p>
396
+ {deleteTarget?.name}:{t("workspaceDeleteConfirm")}
397
+ </p>
398
+ <div className="dialog-actions">
399
+ <button type="button" onClick={() => setDeleteTarget(null)}>
400
+ {t("cancel")}
401
+ </button>
402
+ <button
403
+ type="button"
404
+ className="danger"
405
+ onClick={() => {
406
+ if (!deleteTarget) return;
407
+ void props.actions.removeWorkspace(deleteTarget.path);
408
+ setDeleteTarget(null);
409
+ }}
410
+ >
411
+ {t("deleteWorkspace")}
412
+ </button>
413
+ </div>
414
+ </div>
415
+ </Dialog>
416
+ </aside>
417
+ );
418
+ }