@saadnvd1/agent-os 0.3.836 → 0.3.839
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.
- package/app/api/fresh-start/route.test.ts +33 -0
- package/app/api/fresh-start/route.ts +3 -0
- package/components/Devices/DeviceRow.tsx +83 -69
- package/components/Devices/PasskeysSection.test.ts +89 -0
- package/components/Devices/PasskeysSection.tsx +36 -26
- package/components/Health/HealthPanel.tsx +41 -16
- package/components/Merge/GlobalMergeSection.tsx +2 -5
- package/components/Merge/MergeSettingsFields.tsx +39 -77
- package/components/Merge/ProjectMergeSection.tsx +2 -6
- package/components/Merge/WorktreeSweepSection.tsx +40 -14
- package/components/Models/AgentRoleCard.tsx +61 -66
- package/components/Models/AgentRolesSection.tsx +5 -8
- package/components/Models/ModelsSection.tsx +7 -18
- package/components/Models/PriceInput.tsx +59 -0
- package/components/PhoneNotify/PhoneNotifyForm.tsx +37 -58
- package/components/Settings/AppWideLimits.tsx +61 -0
- package/components/Settings/ContextSection.tsx +96 -0
- package/components/Settings/ExperimentsSection.test.ts +9 -7
- package/components/Settings/ExperimentsSection.tsx +22 -23
- package/components/Settings/InlineConfirm.tsx +51 -0
- package/components/Settings/OwnerNameRow.tsx +10 -40
- package/components/Settings/PickerSetting.tsx +59 -0
- package/components/Settings/SettingRow.tsx +131 -0
- package/components/Settings/SettingsDialog.test.ts +1 -0
- package/components/Settings/SettingsDialog.tsx +47 -2
- package/components/Settings/settings-fields.test.ts +103 -0
- package/components/Settings/settings-fields.tsx +427 -0
- package/components/Shares/SharedLinksPanel.tsx +62 -53
- package/components/Workspaces/HqFolderForm.tsx +21 -54
- package/components/Workspaces/McpServersForm.tsx +20 -19
- package/components/Workspaces/ProjectTiersForm.tsx +10 -8
- package/components/Workspaces/RulesForm.test.ts +127 -0
- package/components/Workspaces/RulesForm.tsx +54 -126
- package/components/Workspaces/WorkspaceModelsForm.tsx +27 -26
- package/components/Workspaces/WorkspaceSettings.tsx +61 -24
- package/content/whats-new.json +16 -0
- package/data/fresh-start/index.ts +1 -1
- package/data/workspaces/index.ts +9 -0
- package/hooks/useAutoSave.test.ts +116 -0
- package/hooks/useAutoSave.ts +98 -0
- package/lib/away/window-limits.ts +3 -1
- package/lib/chat/refresh.ts +10 -6
- package/lib/chat/stall-limits.ts +3 -1
- package/lib/settings/defaults.ts +27 -0
- package/lib/settings/parse.ts +17 -0
- package/lib/settings-url.ts +1 -0
- package/lib/worktree-sweep/limits.ts +3 -0
- package/lib/worktree-sweep/settings.ts +4 -2
- package/package.json +1 -1
- package/components/Away/AwayWindowForm.tsx +0 -91
- package/components/Workspaces/FreshStartForm.tsx +0 -177
- package/components/Workspaces/StallForm.tsx +0 -101
- package/components/Workspaces/TaskLimitForm.tsx +0 -84
- package/components/Workspaces/fresh-start.ts +0 -19
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from "@/components/ui/select";
|
|
13
|
+
import { Switch } from "@/components/ui/switch";
|
|
14
|
+
import { useAutoSave } from "@/hooks/useAutoSave";
|
|
15
|
+
import { parseSettingNumber } from "@/lib/settings/parse";
|
|
16
|
+
import { InlineConfirm } from "./InlineConfirm";
|
|
17
|
+
import { SettingRow } from "./SettingRow";
|
|
18
|
+
|
|
19
|
+
type Save<T> = (next: T) => Promise<unknown>;
|
|
20
|
+
|
|
21
|
+
const FIELD = "h-11 text-base md:h-9 md:text-sm";
|
|
22
|
+
|
|
23
|
+
/** A switch: saves at once. `confirm` makes turning it ON ask first. */
|
|
24
|
+
export function SwitchSetting({
|
|
25
|
+
id,
|
|
26
|
+
label,
|
|
27
|
+
help,
|
|
28
|
+
checked,
|
|
29
|
+
onSave,
|
|
30
|
+
defaultValue,
|
|
31
|
+
confirm,
|
|
32
|
+
confirmLabel = "Turn on",
|
|
33
|
+
undoable,
|
|
34
|
+
locked,
|
|
35
|
+
extra,
|
|
36
|
+
}: {
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
help?: string;
|
|
40
|
+
checked: boolean;
|
|
41
|
+
onSave: Save<boolean>;
|
|
42
|
+
defaultValue?: boolean;
|
|
43
|
+
confirm?: string;
|
|
44
|
+
confirmLabel?: string;
|
|
45
|
+
undoable?: boolean;
|
|
46
|
+
// Can't be changed from here.
|
|
47
|
+
locked?: boolean;
|
|
48
|
+
// Shown under the row (usage, links).
|
|
49
|
+
extra?: ReactNode;
|
|
50
|
+
}) {
|
|
51
|
+
const auto = useAutoSave({ saved: checked, save: onSave });
|
|
52
|
+
const [asking, setAsking] = useState(false);
|
|
53
|
+
return (
|
|
54
|
+
<SettingRow
|
|
55
|
+
id={id}
|
|
56
|
+
label={label}
|
|
57
|
+
help={locked ? `${help ?? ""} Can't be changed from here.`.trim() : help}
|
|
58
|
+
status={auto.status}
|
|
59
|
+
onUndo={auto.undo}
|
|
60
|
+
hint={
|
|
61
|
+
defaultValue === undefined
|
|
62
|
+
? undefined
|
|
63
|
+
: `default ${defaultValue ? "on" : "off"}`
|
|
64
|
+
}
|
|
65
|
+
onReset={
|
|
66
|
+
defaultValue !== undefined && auto.value !== defaultValue
|
|
67
|
+
? () => void auto.commit(defaultValue, { undoable: true })
|
|
68
|
+
: undefined
|
|
69
|
+
}
|
|
70
|
+
controlClassName="max-sm:col-span-1 max-sm:row-start-1 max-sm:mt-0"
|
|
71
|
+
control={
|
|
72
|
+
<Switch
|
|
73
|
+
id={id}
|
|
74
|
+
checked={auto.value}
|
|
75
|
+
disabled={locked}
|
|
76
|
+
aria-describedby={`${id}-hint`}
|
|
77
|
+
className="data-[state=checked]:bg-primary"
|
|
78
|
+
onCheckedChange={(on) => {
|
|
79
|
+
if (on && confirm) setAsking(true);
|
|
80
|
+
else void auto.commit(on, { undoable: undoable || !on });
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
}
|
|
84
|
+
below={
|
|
85
|
+
(asking || extra) && (
|
|
86
|
+
<>
|
|
87
|
+
{extra}
|
|
88
|
+
{asking && confirm && (
|
|
89
|
+
<InlineConfirm
|
|
90
|
+
label={`Turn on ${label}`}
|
|
91
|
+
message={confirm}
|
|
92
|
+
confirmLabel={confirmLabel}
|
|
93
|
+
onConfirm={() => {
|
|
94
|
+
setAsking(false);
|
|
95
|
+
void auto.commit(true, { undoable: true });
|
|
96
|
+
}}
|
|
97
|
+
onCancel={() => setAsking(false)}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
</>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A number, saved on blur or Enter. Esc reverts; a bad value stays in the
|
|
109
|
+
* field with a message and nothing is sent. `scale` converts the stored value
|
|
110
|
+
* to what's typed (tokens to k). With `optional`, a switch turns it off
|
|
111
|
+
* (`off`) or on (`on`) and the input greys out, keeping its number.
|
|
112
|
+
*/
|
|
113
|
+
export function NumberSetting({
|
|
114
|
+
id,
|
|
115
|
+
label,
|
|
116
|
+
help,
|
|
117
|
+
value,
|
|
118
|
+
onSave,
|
|
119
|
+
defaultValue,
|
|
120
|
+
min,
|
|
121
|
+
max,
|
|
122
|
+
integer = true,
|
|
123
|
+
unit,
|
|
124
|
+
short = "",
|
|
125
|
+
scale = 1,
|
|
126
|
+
optional,
|
|
127
|
+
emptyResets,
|
|
128
|
+
}: {
|
|
129
|
+
id: string;
|
|
130
|
+
label: string;
|
|
131
|
+
help?: string;
|
|
132
|
+
value: number;
|
|
133
|
+
onSave: Save<number>;
|
|
134
|
+
defaultValue: number;
|
|
135
|
+
min: number;
|
|
136
|
+
max?: number;
|
|
137
|
+
integer?: boolean;
|
|
138
|
+
unit?: string;
|
|
139
|
+
// The unit as it follows a number in the hint ("k").
|
|
140
|
+
short?: string;
|
|
141
|
+
scale?: number;
|
|
142
|
+
optional?: { off: number; on: number };
|
|
143
|
+
// Clearing the box resets to the default instead of being an error.
|
|
144
|
+
emptyResets?: boolean;
|
|
145
|
+
}) {
|
|
146
|
+
const auto = useAutoSave({ saved: value, save: onSave });
|
|
147
|
+
const [text, setText] = useState<string | null>(null);
|
|
148
|
+
const [error, setError] = useState<string | null>(null);
|
|
149
|
+
const off = optional ? auto.value === optional.off : false;
|
|
150
|
+
const shown = (off ? optional!.on : auto.value) / scale;
|
|
151
|
+
const defOff = optional ? defaultValue === optional.off : false;
|
|
152
|
+
const hint =
|
|
153
|
+
error === null
|
|
154
|
+
? `default ${defOff ? "off" : `${defaultValue / scale}${short}`} · ${
|
|
155
|
+
max === undefined
|
|
156
|
+
? `${min / scale}${short}+`
|
|
157
|
+
: `${min / scale}–${max / scale}${short}`
|
|
158
|
+
}`
|
|
159
|
+
: undefined;
|
|
160
|
+
|
|
161
|
+
const finish = () => {
|
|
162
|
+
if (text === null) return;
|
|
163
|
+
if (!text.trim() && emptyResets) {
|
|
164
|
+
setText(null);
|
|
165
|
+
void auto.commit(defaultValue, { undoable: true });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const parsed = parseSettingNumber(text, {
|
|
169
|
+
min: min / scale,
|
|
170
|
+
max: max === undefined ? undefined : max / scale,
|
|
171
|
+
integer,
|
|
172
|
+
});
|
|
173
|
+
if ("error" in parsed) return setError(parsed.error);
|
|
174
|
+
setError(null);
|
|
175
|
+
setText(null);
|
|
176
|
+
const next = Math.round(parsed.value * scale * 1e6) / 1e6;
|
|
177
|
+
void auto.commit(next, {
|
|
178
|
+
undoable:
|
|
179
|
+
auto.value > 0 && Math.abs(next - auto.value) / auto.value > 0.5,
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<SettingRow
|
|
185
|
+
id={id}
|
|
186
|
+
label={label}
|
|
187
|
+
help={help}
|
|
188
|
+
status={auto.status}
|
|
189
|
+
error={error}
|
|
190
|
+
hint={hint}
|
|
191
|
+
onUndo={auto.undo}
|
|
192
|
+
onReset={
|
|
193
|
+
auto.value !== defaultValue
|
|
194
|
+
? () => {
|
|
195
|
+
setText(null);
|
|
196
|
+
setError(null);
|
|
197
|
+
void auto.commit(defaultValue, { undoable: true });
|
|
198
|
+
}
|
|
199
|
+
: undefined
|
|
200
|
+
}
|
|
201
|
+
control={
|
|
202
|
+
<>
|
|
203
|
+
<Input
|
|
204
|
+
id={id}
|
|
205
|
+
type="text"
|
|
206
|
+
inputMode={integer ? "numeric" : "decimal"}
|
|
207
|
+
value={text ?? String(shown)}
|
|
208
|
+
disabled={off}
|
|
209
|
+
aria-invalid={!!error}
|
|
210
|
+
aria-describedby={`${id}-hint`}
|
|
211
|
+
onChange={(e) => {
|
|
212
|
+
setText(e.target.value);
|
|
213
|
+
if (error) setError(null);
|
|
214
|
+
}}
|
|
215
|
+
onBlur={finish}
|
|
216
|
+
onKeyDown={(e) => {
|
|
217
|
+
if (e.key === "Enter") e.currentTarget.blur();
|
|
218
|
+
if (e.key === "Escape") {
|
|
219
|
+
setText(null);
|
|
220
|
+
setError(null);
|
|
221
|
+
e.currentTarget.blur();
|
|
222
|
+
}
|
|
223
|
+
}}
|
|
224
|
+
className={`${FIELD} w-24 text-right tabular-nums`}
|
|
225
|
+
/>
|
|
226
|
+
{unit && (
|
|
227
|
+
<span className="text-muted-foreground min-w-16 text-sm">
|
|
228
|
+
{unit}
|
|
229
|
+
</span>
|
|
230
|
+
)}
|
|
231
|
+
{optional && (
|
|
232
|
+
<Switch
|
|
233
|
+
aria-label={`${label} on`}
|
|
234
|
+
checked={!off}
|
|
235
|
+
onCheckedChange={(on) => {
|
|
236
|
+
setError(null);
|
|
237
|
+
setText(null);
|
|
238
|
+
void auto.commit(on ? optional.on : optional.off, {
|
|
239
|
+
undoable: !on,
|
|
240
|
+
});
|
|
241
|
+
}}
|
|
242
|
+
/>
|
|
243
|
+
)}
|
|
244
|
+
</>
|
|
245
|
+
}
|
|
246
|
+
/>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** A choice, saved at once. */
|
|
251
|
+
export function SelectSetting({
|
|
252
|
+
id,
|
|
253
|
+
label,
|
|
254
|
+
help,
|
|
255
|
+
value,
|
|
256
|
+
options,
|
|
257
|
+
onSave,
|
|
258
|
+
defaultValue,
|
|
259
|
+
hint,
|
|
260
|
+
}: {
|
|
261
|
+
id: string;
|
|
262
|
+
label: string;
|
|
263
|
+
help?: string;
|
|
264
|
+
value: string;
|
|
265
|
+
options: { value: string; label: string }[];
|
|
266
|
+
onSave: Save<string>;
|
|
267
|
+
defaultValue?: string;
|
|
268
|
+
// Replaces "default <label>" (an empty string for none).
|
|
269
|
+
hint?: string;
|
|
270
|
+
}) {
|
|
271
|
+
const auto = useAutoSave({ saved: value, save: onSave });
|
|
272
|
+
const def = options.find((o) => o.value === defaultValue);
|
|
273
|
+
return (
|
|
274
|
+
<SettingRow
|
|
275
|
+
id={id}
|
|
276
|
+
label={label}
|
|
277
|
+
help={help}
|
|
278
|
+
status={auto.status}
|
|
279
|
+
onUndo={auto.undo}
|
|
280
|
+
hint={hint ?? (def ? `default ${def.label}` : undefined)}
|
|
281
|
+
onReset={
|
|
282
|
+
defaultValue !== undefined && auto.value !== defaultValue
|
|
283
|
+
? () => void auto.commit(defaultValue, { undoable: true })
|
|
284
|
+
: undefined
|
|
285
|
+
}
|
|
286
|
+
control={
|
|
287
|
+
<Select value={auto.value} onValueChange={(v) => void auto.commit(v)}>
|
|
288
|
+
<SelectTrigger id={id} className={`${FIELD} w-44`}>
|
|
289
|
+
<SelectValue />
|
|
290
|
+
</SelectTrigger>
|
|
291
|
+
<SelectContent>
|
|
292
|
+
{options.map((o) => (
|
|
293
|
+
<SelectItem key={o.value} value={o.value}>
|
|
294
|
+
{o.label}
|
|
295
|
+
</SelectItem>
|
|
296
|
+
))}
|
|
297
|
+
</SelectContent>
|
|
298
|
+
</Select>
|
|
299
|
+
}
|
|
300
|
+
/>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Text, saved on blur or Enter after `validate` (sync or async) says it's
|
|
306
|
+
* fine. With `secret` or `explicit`, nothing saves on blur: Enter or the
|
|
307
|
+
* button does (a save that asks for a passkey must be deliberate). `secret`
|
|
308
|
+
* also hides what's typed.
|
|
309
|
+
*/
|
|
310
|
+
export function TextSetting({
|
|
311
|
+
id,
|
|
312
|
+
label,
|
|
313
|
+
help,
|
|
314
|
+
value,
|
|
315
|
+
onSave,
|
|
316
|
+
validate,
|
|
317
|
+
placeholder,
|
|
318
|
+
secret,
|
|
319
|
+
buttonLabel = "Save",
|
|
320
|
+
defaultValue = "",
|
|
321
|
+
maxLength,
|
|
322
|
+
explicit,
|
|
323
|
+
resettable = true,
|
|
324
|
+
after,
|
|
325
|
+
}: {
|
|
326
|
+
id: string;
|
|
327
|
+
label: string;
|
|
328
|
+
help?: string;
|
|
329
|
+
value: string;
|
|
330
|
+
onSave: Save<string>;
|
|
331
|
+
validate?: (text: string) => string | null | Promise<string | null>;
|
|
332
|
+
placeholder?: string;
|
|
333
|
+
secret?: boolean;
|
|
334
|
+
buttonLabel?: string;
|
|
335
|
+
defaultValue?: string;
|
|
336
|
+
maxLength?: number;
|
|
337
|
+
explicit?: boolean;
|
|
338
|
+
// Offer Reset while the value differs from `defaultValue`.
|
|
339
|
+
resettable?: boolean;
|
|
340
|
+
// Extra controls beside the input, such as a delete button.
|
|
341
|
+
after?: ReactNode;
|
|
342
|
+
}) {
|
|
343
|
+
const deliberate = secret || explicit;
|
|
344
|
+
const auto = useAutoSave({ saved: value, save: onSave });
|
|
345
|
+
const [text, setText] = useState<string | null>(null);
|
|
346
|
+
const [error, setError] = useState<string | null>(null);
|
|
347
|
+
const [checking, setChecking] = useState(false);
|
|
348
|
+
|
|
349
|
+
const finish = async () => {
|
|
350
|
+
if (text === null) return;
|
|
351
|
+
const next = text.trim();
|
|
352
|
+
if (next === auto.value) return setText(null);
|
|
353
|
+
setChecking(true);
|
|
354
|
+
const problem = (await validate?.(next)) ?? null;
|
|
355
|
+
setChecking(false);
|
|
356
|
+
if (problem) return setError(problem);
|
|
357
|
+
setError(null);
|
|
358
|
+
setText(null);
|
|
359
|
+
await auto.commit(next, { undoable: !next });
|
|
360
|
+
};
|
|
361
|
+
const dirty = text !== null && text.trim() !== auto.value;
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<SettingRow
|
|
365
|
+
id={id}
|
|
366
|
+
label={label}
|
|
367
|
+
help={help}
|
|
368
|
+
status={checking ? "saving" : auto.status}
|
|
369
|
+
error={error}
|
|
370
|
+
onUndo={auto.undo}
|
|
371
|
+
onReset={
|
|
372
|
+
resettable && !secret && auto.value !== defaultValue
|
|
373
|
+
? () => {
|
|
374
|
+
setText(null);
|
|
375
|
+
setError(null);
|
|
376
|
+
void auto.commit(defaultValue, { undoable: true });
|
|
377
|
+
}
|
|
378
|
+
: undefined
|
|
379
|
+
}
|
|
380
|
+
control={
|
|
381
|
+
<>
|
|
382
|
+
<Input
|
|
383
|
+
id={id}
|
|
384
|
+
type={secret ? "password" : "text"}
|
|
385
|
+
autoComplete="off"
|
|
386
|
+
maxLength={maxLength}
|
|
387
|
+
value={text ?? (secret ? "" : auto.value)}
|
|
388
|
+
placeholder={placeholder}
|
|
389
|
+
aria-invalid={!!error}
|
|
390
|
+
aria-describedby={`${id}-hint`}
|
|
391
|
+
onChange={(e) => {
|
|
392
|
+
setText(e.target.value);
|
|
393
|
+
if (error) setError(null);
|
|
394
|
+
}}
|
|
395
|
+
onBlur={() => {
|
|
396
|
+
if (!deliberate) void finish();
|
|
397
|
+
}}
|
|
398
|
+
onKeyDown={(e) => {
|
|
399
|
+
if (e.key === "Enter") {
|
|
400
|
+
if (deliberate) void finish();
|
|
401
|
+
else e.currentTarget.blur();
|
|
402
|
+
}
|
|
403
|
+
if (e.key === "Escape") {
|
|
404
|
+
setText(null);
|
|
405
|
+
setError(null);
|
|
406
|
+
e.currentTarget.blur();
|
|
407
|
+
}
|
|
408
|
+
}}
|
|
409
|
+
className={`${FIELD} w-full sm:w-64`}
|
|
410
|
+
/>
|
|
411
|
+
{deliberate && (
|
|
412
|
+
<Button
|
|
413
|
+
type="button"
|
|
414
|
+
size="sm"
|
|
415
|
+
className="h-11 sm:h-9"
|
|
416
|
+
disabled={!dirty || checking}
|
|
417
|
+
onClick={() => void finish()}
|
|
418
|
+
>
|
|
419
|
+
{buttonLabel}
|
|
420
|
+
</Button>
|
|
421
|
+
)}
|
|
422
|
+
{after}
|
|
423
|
+
</>
|
|
424
|
+
}
|
|
425
|
+
/>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { toast } from "sonner";
|
|
4
5
|
import { Copy, Link2 } from "lucide-react";
|
|
5
6
|
import { Button } from "@/components/ui/button";
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
} from "@/data/shares";
|
|
12
13
|
import { useAllLiveShares, useEndLive, type LiveView } from "@/data/live";
|
|
13
14
|
import { cn } from "@/lib/utils";
|
|
15
|
+
import { InlineConfirm } from "@/components/Settings/InlineConfirm";
|
|
14
16
|
import { SessionAnchor } from "@/components/Chat/SessionAnchor";
|
|
15
17
|
|
|
16
18
|
const when = (iso: string) =>
|
|
@@ -23,6 +25,7 @@ const when = (iso: string) =>
|
|
|
23
25
|
function Row({ share }: { share: SharedLinkRow }) {
|
|
24
26
|
const revoke = useRevokeShare();
|
|
25
27
|
const live = share.status === "active";
|
|
28
|
+
const [revoking, setRevoking] = useState(false);
|
|
26
29
|
|
|
27
30
|
const copy = async () => {
|
|
28
31
|
try {
|
|
@@ -33,12 +36,7 @@ function Row({ share }: { share: SharedLinkRow }) {
|
|
|
33
36
|
}
|
|
34
37
|
};
|
|
35
38
|
const onRevoke = () => {
|
|
36
|
-
|
|
37
|
-
!window.confirm(
|
|
38
|
-
"Revoke this link? The page is deleted from Connect and the link stops working."
|
|
39
|
-
)
|
|
40
|
-
)
|
|
41
|
-
return;
|
|
39
|
+
setRevoking(false);
|
|
42
40
|
revoke.mutate(share.id, {
|
|
43
41
|
onError: (err) => toast.error((err as Error).message),
|
|
44
42
|
});
|
|
@@ -54,57 +52,68 @@ function Row({ share }: { share: SharedLinkRow }) {
|
|
|
54
52
|
);
|
|
55
53
|
|
|
56
54
|
return (
|
|
57
|
-
<li className="bg-muted/40
|
|
58
|
-
<div className="
|
|
59
|
-
<div className="
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
55
|
+
<li className="bg-muted/40 rounded-lg px-3 py-2.5">
|
|
56
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
57
|
+
<div className="min-w-0 flex-1">
|
|
58
|
+
<div className="truncate text-sm font-medium">{title}</div>
|
|
59
|
+
<div className="text-muted-foreground flex flex-wrap gap-x-3 text-xs">
|
|
60
|
+
<span>{share.kind === "chat" ? "Chat" : "Page"}</span>
|
|
61
|
+
{share.kind === "artifact" && (
|
|
62
|
+
<span>
|
|
63
|
+
{share.follow
|
|
64
|
+
? `Follows latest${share.version ? ` · now v${share.version}` : ""}`
|
|
65
|
+
: `pinned${share.version ? ` v${share.version}` : ""}`}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
<span>Created {when(share.createdAt)}</span>
|
|
63
69
|
<span>
|
|
64
|
-
{share.
|
|
65
|
-
?
|
|
66
|
-
:
|
|
70
|
+
{share.status === "revoked"
|
|
71
|
+
? "Revoked"
|
|
72
|
+
: share.status === "expired"
|
|
73
|
+
? "Expired"
|
|
74
|
+
: share.expiresAt
|
|
75
|
+
? `Expires ${when(share.expiresAt)}`
|
|
76
|
+
: "Never expires"}
|
|
67
77
|
</span>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
<span>
|
|
79
|
+
{share.views === null
|
|
80
|
+
? "Views unavailable"
|
|
81
|
+
: `${share.views} ${share.views === 1 ? "view" : "views"}`}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="flex shrink-0 gap-2">
|
|
86
|
+
<Button
|
|
87
|
+
variant="outline"
|
|
88
|
+
className={cn("min-h-11 sm:min-h-8", !live && "invisible")}
|
|
89
|
+
onClick={copy}
|
|
90
|
+
disabled={!live}
|
|
91
|
+
>
|
|
92
|
+
<Copy className="h-4 w-4" />
|
|
93
|
+
Copy link
|
|
94
|
+
</Button>
|
|
95
|
+
<Button
|
|
96
|
+
variant="outline"
|
|
97
|
+
className={cn(
|
|
98
|
+
"text-destructive min-h-11 sm:min-h-8",
|
|
99
|
+
!live && "invisible"
|
|
100
|
+
)}
|
|
101
|
+
onClick={() => setRevoking(true)}
|
|
102
|
+
disabled={!live || revoke.isPending}
|
|
103
|
+
>
|
|
104
|
+
Revoke
|
|
105
|
+
</Button>
|
|
84
106
|
</div>
|
|
85
107
|
</div>
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
</Button>
|
|
96
|
-
<Button
|
|
97
|
-
variant="outline"
|
|
98
|
-
className={cn(
|
|
99
|
-
"text-destructive min-h-11 sm:min-h-8",
|
|
100
|
-
!live && "invisible"
|
|
101
|
-
)}
|
|
102
|
-
onClick={onRevoke}
|
|
103
|
-
disabled={!live || revoke.isPending}
|
|
104
|
-
>
|
|
105
|
-
Revoke
|
|
106
|
-
</Button>
|
|
107
|
-
</div>
|
|
108
|
+
{revoking && (
|
|
109
|
+
<InlineConfirm
|
|
110
|
+
label="Revoke link"
|
|
111
|
+
message="Revoke this link? The page is deleted from Connect and the link stops working."
|
|
112
|
+
confirmLabel="Revoke"
|
|
113
|
+
onConfirm={onRevoke}
|
|
114
|
+
onCancel={() => setRevoking(false)}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
108
117
|
</li>
|
|
109
118
|
);
|
|
110
119
|
}
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
import { Input } from "@/components/ui/input";
|
|
3
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
4
|
+
import { TextSetting } from "@/components/Settings/settings-fields";
|
|
7
5
|
import { useSetWorkspaceHq, useWorkspaceHqQuery } from "@/data/session-logs";
|
|
8
6
|
|
|
9
7
|
// The git repo this workspace's session logs are written into and
|
|
10
|
-
// committed to. Blank keeps them in AgentOS's own folder.
|
|
8
|
+
// committed to. Blank keeps them in AgentOS's own folder. The server checks
|
|
9
|
+
// the folder exists and is a fit repo before anything is saved; a refusal
|
|
10
|
+
// shows as a toast and the field goes back to the saved folder.
|
|
11
11
|
export function HqFolderForm({ workspaceId }: { workspaceId: string }) {
|
|
12
12
|
const { data, isPending, isError, error } = useWorkspaceHqQuery(workspaceId);
|
|
13
13
|
const save = useSetWorkspaceHq(workspaceId);
|
|
14
|
-
const [draft, setDraft] = useState<string | null>(null);
|
|
15
|
-
const saved = data?.hq?.folder ?? "";
|
|
16
|
-
const folder = draft ?? saved;
|
|
17
|
-
const id = `hq-folder-${workspaceId}`;
|
|
18
14
|
|
|
19
15
|
if (isPending)
|
|
20
16
|
return <div className="bg-muted/60 h-11 animate-pulse rounded-md" />;
|
|
@@ -22,50 +18,21 @@ export function HqFolderForm({ workspaceId }: { workspaceId: string }) {
|
|
|
22
18
|
return <p className="text-destructive text-xs">{error.message}</p>;
|
|
23
19
|
|
|
24
20
|
return (
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
HQ folder
|
|
42
|
-
</label>
|
|
43
|
-
<div className="flex gap-2">
|
|
44
|
-
<Input
|
|
45
|
-
id={id}
|
|
46
|
-
value={folder}
|
|
47
|
-
onChange={(e) => setDraft(e.target.value)}
|
|
48
|
-
placeholder="~/path/to/a/private/git/repo"
|
|
49
|
-
className="h-11 flex-1 text-base md:h-9 md:text-sm"
|
|
50
|
-
/>
|
|
51
|
-
<Button
|
|
52
|
-
type="submit"
|
|
53
|
-
size="sm"
|
|
54
|
-
className="h-11 sm:h-9"
|
|
55
|
-
disabled={folder === saved || save.isPending}
|
|
56
|
-
>
|
|
57
|
-
Save
|
|
58
|
-
</Button>
|
|
59
|
-
</div>
|
|
60
|
-
<p className="text-muted-foreground text-xs">
|
|
61
|
-
Session logs go to docs/sessions/<project>/ in this repo and are
|
|
62
|
-
committed there, one file per commit.{" "}
|
|
63
|
-
{data.hq?.visibility === "private"
|
|
64
|
-
? "The repo is private, so each commit is pushed."
|
|
65
|
-
: data.hq
|
|
66
|
-
? "Commits stay local: the repo isn't a private GitHub repository."
|
|
67
|
-
: "It must be a private git repo outside the workspace's projects."}
|
|
68
|
-
</p>
|
|
69
|
-
</form>
|
|
21
|
+
<SettingSection>
|
|
22
|
+
<TextSetting
|
|
23
|
+
id={`hq-folder-${workspaceId}`}
|
|
24
|
+
label="HQ folder"
|
|
25
|
+
help={`Session logs go to docs/sessions/<project>/ in this repo, one commit per file. ${
|
|
26
|
+
data.hq?.visibility === "private"
|
|
27
|
+
? "The repo is private, so each commit is pushed."
|
|
28
|
+
: data.hq
|
|
29
|
+
? "Commits stay local: the repo isn't a private GitHub repository."
|
|
30
|
+
: "It must be a private git repo outside the workspace's projects."
|
|
31
|
+
}`}
|
|
32
|
+
placeholder="~/path/to/a/private/git/repo"
|
|
33
|
+
value={data.hq?.folder ?? ""}
|
|
34
|
+
onSave={(folder) => save.mutateAsync(folder)}
|
|
35
|
+
/>
|
|
36
|
+
</SettingSection>
|
|
70
37
|
);
|
|
71
38
|
}
|