@talgent/workbench-ui 0.0.0 → 0.2.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.
- package/README.md +15 -2
- package/assets/brand-icons/LICENSE.md +30 -0
- package/assets/brand-icons/README.md +5 -0
- package/assets/brand-icons/figma.svg +1 -0
- package/assets/brand-icons/github.svg +1 -0
- package/assets/brand-icons/linear.svg +1 -0
- package/assets/brand-icons/notion.svg +1 -0
- package/assets/brand-icons/sentry.svg +1 -0
- package/assets/brand-icons/slack.svg +1 -0
- package/assets/fonts/Inter-OFL.txt +93 -0
- package/assets/fonts/inter-latin.woff2 +0 -0
- package/components/agent/agent-icon.css +14 -0
- package/components/agent/agent-icon.tsx +14 -0
- package/components/analytics/chart-geometry.ts +95 -0
- package/components/analytics/dashboard-charts.tsx +136 -0
- package/components/analytics/model-usage-chart.tsx +84 -0
- package/components/analytics/token-breakdown.css +13 -0
- package/components/analytics/token-breakdown.tsx +23 -0
- package/components/analytics/token-report.tsx +57 -0
- package/components/analytics/work-usage/turn-timeline.tsx +98 -0
- package/components/analytics/work-usage/turns.ts +170 -0
- package/components/analytics/work-usage/work-status.ts +36 -0
- package/components/analytics/work-usage/work-usage-reader.tsx +122 -0
- package/components/analytics/work-usage/work-usage.css +48 -0
- package/components/auth/auth-gate.tsx +44 -0
- package/components/chat/message-input.tsx +105 -0
- package/components/composer/permission-mode-list.tsx +15 -0
- package/components/composer/prompt-bar-controls.tsx +77 -0
- package/components/composer/prompt-bar.tsx +45 -0
- package/components/composer/skill-completion.css +27 -0
- package/components/composer/skill-completion.tsx +49 -0
- package/components/composer/use-slash-completion.tsx +69 -0
- package/components/composer/work-start.css +133 -0
- package/components/feedback/toast-region.css +2 -0
- package/components/feedback/toast-region.tsx +15 -0
- package/components/files/file-facts.tsx +16 -0
- package/components/files/file-section.tsx +181 -0
- package/components/markdown.tsx +144 -0
- package/components/marketplace/connector-pages.tsx +166 -0
- package/components/marketplace/marketplace-icons.tsx +66 -0
- package/components/marketplace/marketplace-shared.tsx +43 -0
- package/components/marketplace/marketplace-shell.tsx +32 -0
- package/components/marketplace/marketplace.css +97 -0
- package/components/marketplace/parameter-fields.tsx +46 -0
- package/components/marketplace/plugin-list.tsx +84 -0
- package/components/marketplace/plugin-page.tsx +133 -0
- package/components/marketplace/provider-pages.tsx +120 -0
- package/components/preview/file-format.ts +75 -0
- package/components/preview/file-preview.tsx +153 -0
- package/components/preview/preview-verdict.ts +44 -0
- package/components/preview/preview.css +19 -0
- package/components/preview/text-preview.tsx +174 -0
- package/components/profile/provider-callback.tsx +43 -0
- package/components/profile/work-agent-appearance.tsx +109 -0
- package/components/profile/work-agent-soul.tsx +76 -0
- package/components/rail/rail-section.tsx +20 -0
- package/components/rail/rail.css +19 -0
- package/components/rail/use-expand-on-change.ts +18 -0
- package/components/session/startup-progress.css +77 -0
- package/components/session/startup-progress.tsx +103 -0
- package/components/session/work-permission-control.tsx +45 -0
- package/components/session/work-thinking-control.tsx +52 -0
- package/components/shell/account-menu.tsx +134 -0
- package/components/shell/app-shell.tsx +148 -0
- package/components/shell/core-reading-views.tsx +26 -0
- package/components/shell/core-reading.tsx +17 -0
- package/components/shell/document-title.tsx +11 -0
- package/components/shell/entry-shell.ts +39 -0
- package/components/shell/file-reading.tsx +22 -0
- package/components/shell/page-layout.tsx +7 -0
- package/components/shell/reading-context.ts +41 -0
- package/components/shell/reading-panel.css +67 -0
- package/components/shell/reading-panel.tsx +74 -0
- package/components/shell/sidebar-parts.tsx +38 -0
- package/components/work/context-rail.css +61 -0
- package/components/work/conversation-unavailable.css +6 -0
- package/components/work/conversation-unavailable.tsx +11 -0
- package/components/work/subagent-item.tsx +42 -0
- package/components/work/subagents-section.css +31 -0
- package/components/work/subagents-section.tsx +40 -0
- package/components/work/token-usage.tsx +28 -0
- package/components/work/tool-message-detail.tsx +39 -0
- package/components/work/tool-output-preview.tsx +52 -0
- package/components/work/tool-preview-reading.tsx +27 -0
- package/components/work/tool-reading-content.tsx +31 -0
- package/components/work/work-context-rail.tsx +23 -0
- package/components/work/work-conversation.tsx +207 -0
- package/components/work/work-history.tsx +115 -0
- package/components/work/work-interaction.css +38 -0
- package/components/work/work-interaction.tsx +105 -0
- package/components/work/work-message-flow.css +86 -0
- package/components/work/work-message-flow.tsx +352 -0
- package/components/work/work-progress-section.tsx +20 -0
- package/components/work/work-prompt-bar.tsx +82 -0
- package/components/work/work-question-record.tsx +72 -0
- package/components/work/work-snapshot-state.tsx +24 -0
- package/components/work/work-status-control.tsx +54 -0
- package/components/work-start/start-work-launch-state.ts +33 -0
- package/components/work-start/work-start-catalog.tsx +116 -0
- package/components/work-start/work-start-form.tsx +173 -0
- package/components/work-start/work-start-tray.css +40 -0
- package/components/work-start/work-start-tray.tsx +128 -0
- package/hooks/use-debounced.ts +11 -0
- package/hooks/use-work-conversation.ts +125 -0
- package/hooks/use-work-lifecycle-controls.ts +40 -0
- package/hooks/use-work-stream.ts +91 -0
- package/lib/account-service.ts +77 -0
- package/lib/agent-appearance-service.ts +59 -0
- package/lib/agent-icon.ts +26 -0
- package/lib/agent-soul-service.ts +48 -0
- package/lib/agent-work-service.ts +23 -0
- package/lib/client.ts +20 -0
- package/lib/computer-release.ts +30 -0
- package/lib/computer-service.ts +241 -0
- package/lib/dashboard-report.ts +38 -0
- package/lib/dashboard.ts +21 -0
- package/lib/marketplace-catalog.ts +138 -0
- package/lib/marketplace-connections.ts +98 -0
- package/lib/marketplace-plugins.ts +53 -0
- package/lib/marketplace-routes.ts +12 -0
- package/lib/model-vendors.ts +13 -0
- package/lib/name-taken-error.ts +7 -0
- package/lib/notifications.ts +37 -0
- package/lib/permission-modes.ts +25 -0
- package/lib/personal-usage-service.ts +19 -0
- package/lib/personal-work-service.ts +159 -0
- package/lib/plugin-parameters.ts +64 -0
- package/lib/preview-verdict.ts +26 -0
- package/lib/relative-time.ts +11 -0
- package/lib/session.ts +108 -0
- package/lib/share-link.ts +4 -0
- package/lib/stale-build-reload.ts +41 -0
- package/lib/start-work-request.ts +34 -0
- package/lib/startup-progress.ts +9 -0
- package/lib/subagents.ts +12 -0
- package/lib/thinking-levels.ts +2 -0
- package/lib/token-analytics.ts +168 -0
- package/lib/token-usage.ts +64 -0
- package/lib/trace-view-model.ts +292 -0
- package/lib/transport.ts +29 -0
- package/lib/use-now.ts +11 -0
- package/lib/use-work-trace.ts +175 -0
- package/lib/user-display.ts +49 -0
- package/lib/work-conversation.ts +118 -0
- package/lib/work-file-service.ts +118 -0
- package/lib/work-model.ts +38 -0
- package/lib/work-notices.ts +23 -0
- package/lib/work-progress.ts +41 -0
- package/lib/work-projection.ts +122 -0
- package/lib/work-questions.ts +18 -0
- package/lib/work-start-draft.ts +61 -0
- package/lib/work-start-loadout.ts +100 -0
- package/lib/work-start-source.ts +16 -0
- package/lib/work-state-presentation.ts +80 -0
- package/lib/work-stream-source.ts +32 -0
- package/package.json +81 -5
- package/pages/computers/computers-page.tsx +56 -0
- package/pages/computers/connect-computer-page.tsx +92 -0
- package/pages/computers/install-commands.tsx +42 -0
- package/pages/computers/remove-computer-dialog.tsx +33 -0
- package/pages/computers/rename-computer-dialog.tsx +37 -0
- package/pages/computers/room-rules-dialog.tsx +55 -0
- package/pages/login-page.css +33 -0
- package/pages/login-page.tsx +88 -0
- package/pages/login-threads.tsx +51 -0
- package/pages/marketplace-page.tsx +59 -0
- package/pages/profile-page.tsx +68 -0
- package/pages/workbench/library-page.tsx +88 -0
- package/pages/workbench/new-work-page.tsx +24 -0
- package/pages/workbench/person.tsx +13 -0
- package/pages/workbench/personal-work-context.tsx +93 -0
- package/pages/workbench/personal-work-page.tsx +100 -0
- package/pages/workbench/personal-works-page.tsx +36 -0
- package/pages/workbench/share-dialog.tsx +57 -0
- package/pages/workbench/title-dialog.tsx +35 -0
- package/pages/workbench/usage-page.tsx +52 -0
- package/public/assets/brand/talgent-lettering.png +0 -0
- package/public/assets/brand/talgent-logo-page-bg.png +0 -0
- package/public/assets/brand/talgent-logo-white-bg.png +0 -0
- package/public/assets/brand/talgent-mark-page-bg.png +0 -0
- package/public/assets/brand/talgent-mark-white-bg.png +0 -0
- package/public/assets/brand/talgent-mark.png +0 -0
- package/public/assets/product-icons/LICENSE +21 -0
- package/public/assets/product-icons/README.md +13 -0
- package/public/assets/product-icons/anthropic.svg +1 -0
- package/public/assets/product-icons/deepseek.svg +1 -0
- package/public/assets/product-icons/glm.svg +1 -0
- package/public/assets/product-icons/kimi.svg +1 -0
- package/public/assets/product-icons/linear.svg +1 -0
- package/public/assets/product-icons/minimax.svg +1 -0
- package/public/assets/product-icons/notion.svg +1 -0
- package/public/assets/product-icons/openai.svg +1 -0
- package/public/assets/product-icons/qwen.svg +1 -0
- package/styles/base.css +95 -0
- package/styles/fonts.css +14 -0
- package/styles/markdown.css +79 -0
- package/styles/uber-theme.css +96 -0
- package/styles/work-composer.css +222 -0
- package/styles/work-stream.css +343 -0
- package/styles/workbench.css +144 -0
- package/styles/workspace.css +260 -0
- package/test-support/backend.ts +67 -0
- package/test-support/plugin-summary.ts +8 -0
- package/test-support/setup.ts +59 -0
- package/vite-preset.ts +85 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ModelVendor } from "@talgent/workbench-contracts/agent/public/v1/model_pb";
|
|
2
|
+
|
|
3
|
+
// Each Model Vendor the platform knows has a bundled mark; see product-icons/README.md.
|
|
4
|
+
export const modelVendors: Record<ModelVendor, { label: string; icon?: string }> = {
|
|
5
|
+
[ModelVendor.UNSPECIFIED]: { label: "Model" },
|
|
6
|
+
[ModelVendor.ANTHROPIC]: { label: "Anthropic", icon: "anthropic" },
|
|
7
|
+
[ModelVendor.OPENAI]: { label: "OpenAI", icon: "openai" },
|
|
8
|
+
[ModelVendor.DEEPSEEK]: { label: "DeepSeek", icon: "deepseek" },
|
|
9
|
+
[ModelVendor.ZHIPU]: { label: "Zhipu", icon: "glm" },
|
|
10
|
+
[ModelVendor.KIMI]: { label: "Kimi", icon: "kimi" },
|
|
11
|
+
[ModelVendor.QWEN]: { label: "Qwen", icon: "qwen" },
|
|
12
|
+
[ModelVendor.MINIMAX]: { label: "MiniMax", icon: "minimax" },
|
|
13
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// NameTakenError is a Create refused because the name is taken; the person
|
|
2
|
+
// may replace that Attachment on purpose instead.
|
|
3
|
+
export class NameTakenError extends Error {
|
|
4
|
+
constructor(readonly fileName: string) {
|
|
5
|
+
super(`An attachment named ${fileName} already exists.`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ToastQueue } from "@heroui/react";
|
|
2
|
+
|
|
3
|
+
// One global notification entry. Non-blocking results and background
|
|
4
|
+
// refresh failures are Toasts; problems handled in the current flow use a
|
|
5
|
+
// regional Alert instead. A key identifies the operation, so a new notice for
|
|
6
|
+
// the same operation replaces the previous one instead of stacking.
|
|
7
|
+
export type NotificationKind = "default" | "success" | "warning" | "danger";
|
|
8
|
+
export interface Notification {
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
kind: NotificationKind;
|
|
12
|
+
action?: { label: string; onPress: () => void };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const notificationQueue = new ToastQueue<Notification>({ maxVisibleToasts: 3 });
|
|
16
|
+
const open = new Map<string, string>();
|
|
17
|
+
|
|
18
|
+
export function notify(key: string, kind: NotificationKind, title: string, description?: string, options: { timeout?: number; action?: Notification["action"] } = {}) {
|
|
19
|
+
// Danger notices stay until dismissed; HeroUI has no in-place update, so the
|
|
20
|
+
// notice for the same operation is replaced.
|
|
21
|
+
const timeout = options.timeout ?? (kind === "danger" ? 0 : 5000);
|
|
22
|
+
const existing = open.get(key);
|
|
23
|
+
if (existing) notificationQueue.close(existing);
|
|
24
|
+
const id = notificationQueue.add({ title, description, kind, action: options.action }, {
|
|
25
|
+
timeout: timeout || undefined,
|
|
26
|
+
onClose: () => { if (open.get(key) === id) open.delete(key); },
|
|
27
|
+
});
|
|
28
|
+
open.set(key, id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Recovery clears the failure notice of the same operation.
|
|
32
|
+
export function dismiss(key: string) {
|
|
33
|
+
const id = open.get(key);
|
|
34
|
+
if (!id) return;
|
|
35
|
+
notificationQueue.close(id);
|
|
36
|
+
open.delete(key);
|
|
37
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PermissionMode } from "@talgent/workbench-contracts/agent/public/v1/harness_pb";
|
|
2
|
+
|
|
3
|
+
// How much a Managed Work's Harness asks before it acts, as the person picks
|
|
4
|
+
// it at start and while the Work runs. A Cloud Work has no permission mode.
|
|
5
|
+
export type PermissionModeOption = { mode: PermissionMode; name: string; short: string; description: string };
|
|
6
|
+
|
|
7
|
+
export const permissionModes: PermissionModeOption[] = [
|
|
8
|
+
{ mode: PermissionMode.ASK, name: "Ask", short: "Ask", description: "Asks before each action its rules don’t allow" },
|
|
9
|
+
{ mode: PermissionMode.ACCEPT_EDITS, name: "Accept edits", short: "Accept edits", description: "Edits files without asking" },
|
|
10
|
+
{ mode: PermissionMode.PLAN, name: "Plan", short: "Plan", description: "Changes nothing until its plan is approved" },
|
|
11
|
+
{ mode: PermissionMode.BYPASS_PERMISSIONS, name: "Bypass permissions", short: "Bypass", description: "Acts without asking" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// Modes the Harness may report it switched to by itself; no one picks them.
|
|
15
|
+
const reportedPermissionModes: PermissionModeOption[] = [
|
|
16
|
+
{ mode: PermissionMode.DONT_ASK, name: "Don’t ask", short: "Don’t ask", description: "Denies what its rules don’t allow, without asking" },
|
|
17
|
+
{ mode: PermissionMode.AUTO, name: "Auto", short: "Auto", description: "Decides by itself what to ask" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/** A Managed start's default: the Harness acts without asking. */
|
|
21
|
+
export const defaultPermissionMode = PermissionMode.BYPASS_PERMISSIONS;
|
|
22
|
+
|
|
23
|
+
export function permissionModeOption(mode: PermissionMode) {
|
|
24
|
+
return permissionModes.find(option => option.mode === mode) ?? reportedPermissionModes.find(option => option.mode === mode);
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
2
|
+
import { useCurrentUser } from "./account-service";
|
|
3
|
+
import { traceClient } from "./client";
|
|
4
|
+
import type { DashboardReportPeriod } from "./dashboard-report";
|
|
5
|
+
import type { DashboardWindow } from "./dashboard";
|
|
6
|
+
import { useTokenAnalytics } from "./token-analytics";
|
|
7
|
+
import { traceMetricKinds } from "./trace-view-model";
|
|
8
|
+
|
|
9
|
+
/** The person's usage across all their Works, with or without a Project, as
|
|
10
|
+
* Spend shows a Project's. */
|
|
11
|
+
export function usePersonalTokenAnalytics(window: DashboardWindow, period: DashboardReportPeriod) {
|
|
12
|
+
const userId = useCurrentUser().data?.user?.id ?? "";
|
|
13
|
+
return useTokenAnalytics(["personal-token", userId], bounds => traceClient.getPersonalTraceMetrics({
|
|
14
|
+
startTime: timestampFromDate(bounds.start),
|
|
15
|
+
endTime: timestampFromDate(bounds.end),
|
|
16
|
+
stepSeconds: bounds.stepSeconds,
|
|
17
|
+
metricKinds: [...traceMetricKinds],
|
|
18
|
+
}), window, period, Boolean(userId));
|
|
19
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { timestampDate, timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import type { DirectoryUser, LibraryFile, LibraryWork, PersonalWorkSummary } from "@talgent/workbench-contracts/agent/public/v1/personal_work_pb";
|
|
5
|
+
import { useCurrentUser } from "./account-service";
|
|
6
|
+
import { agentWorkQueryKey } from "./agent-work-service";
|
|
7
|
+
import { agentWorkClient, personalWorkClient } from "./client";
|
|
8
|
+
import { workStartFields, type WorkStartInput } from "./start-work-request";
|
|
9
|
+
import type { WorkStartSource } from "./work-start-source";
|
|
10
|
+
|
|
11
|
+
// A person's own use of their Agent (Agent's PersonalWorkService): the
|
|
12
|
+
// Personal Works they start for themselves, sharing them, and their Library.
|
|
13
|
+
|
|
14
|
+
/** stateReason: why it stopped; disconnectedSince: when it was disconnected,
|
|
15
|
+
* null while observed. */
|
|
16
|
+
export type PersonalWork = { id: string; title: string; state: string; stateReason: string; waiting: boolean; updatedAt: Date | null; disconnectedSince: Date | null };
|
|
17
|
+
|
|
18
|
+
function personalWork(work: PersonalWorkSummary): PersonalWork {
|
|
19
|
+
return { id: work.workId, title: work.title.trim() || "Untitled work", state: work.state, stateReason: work.stateReason, waiting: work.waiting, updatedAt: work.updatedAt ? timestampDate(work.updatedAt) : null, disconnectedSince: work.disconnectedSince ? timestampDate(work.disconnectedSince) : null };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Every person's Personal Works lists, the prefix of personalWorksQueryKey. */
|
|
23
|
+
export const personalWorksKey = ["personal-works"] as const;
|
|
24
|
+
|
|
25
|
+
export function personalWorksQueryKey(userId: string | undefined) {
|
|
26
|
+
return [...personalWorksKey, userId ?? ""] as const;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** How a Work is named among a person's own Works when the Workbench holds no
|
|
30
|
+
* title for it, such as a Work another product started. */
|
|
31
|
+
export const untitledWorkLabel = "Work";
|
|
32
|
+
|
|
33
|
+
/** The person's Personal Works, closed ones included, most recently updated first. */
|
|
34
|
+
export function usePersonalWorks() {
|
|
35
|
+
const userId = useCurrentUser().data?.user?.id;
|
|
36
|
+
return useQuery({
|
|
37
|
+
queryKey: personalWorksQueryKey(userId),
|
|
38
|
+
queryFn: async ({ signal }) => (await personalWorkClient.listPersonalWorks({ limit: 100 }, { signal })).works.map(personalWork),
|
|
39
|
+
enabled: Boolean(userId),
|
|
40
|
+
retry: false,
|
|
41
|
+
refetchInterval: 10000,
|
|
42
|
+
refetchOnWindowFocus: false,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useChangePersonalWorkTitle(workId: string) {
|
|
47
|
+
const client = useQueryClient();
|
|
48
|
+
return useMutation({
|
|
49
|
+
mutationFn: async (title: string) => (await personalWorkClient.changePersonalWorkTitle({ workId, title })).title,
|
|
50
|
+
onSuccess: () => { void client.invalidateQueries({ queryKey: personalWorksKey }); },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** People in the User Directory whose username matches the query. */
|
|
55
|
+
export function useShareCandidates(query: string) {
|
|
56
|
+
const text = query.trim();
|
|
57
|
+
return useQuery({
|
|
58
|
+
queryKey: ["share-candidates", text],
|
|
59
|
+
queryFn: async ({ signal }) => (await personalWorkClient.searchShareReaders({ query: text, limit: 10 }, { signal })).users,
|
|
60
|
+
enabled: text.length > 0,
|
|
61
|
+
retry: false,
|
|
62
|
+
placeholderData: keepPreviousData,
|
|
63
|
+
staleTime: 30_000,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The people behind the given identities, by identity; an unknown one is absent. */
|
|
68
|
+
export function useDirectoryUsers(userIds: string[]) {
|
|
69
|
+
const ids = [...new Set(userIds.filter(Boolean))].sort();
|
|
70
|
+
return useQuery({
|
|
71
|
+
queryKey: ["directory-users", ids],
|
|
72
|
+
queryFn: async ({ signal }) => new Map((await personalWorkClient.getShareReaders({ userIds: ids }, { signal })).users.map(user => [user.userId, user] as const)),
|
|
73
|
+
enabled: ids.length > 0,
|
|
74
|
+
retry: false,
|
|
75
|
+
staleTime: 60_000,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useGrantPersonalWorkShare(workId: string) {
|
|
80
|
+
const client = useQueryClient();
|
|
81
|
+
return useMutation({
|
|
82
|
+
mutationFn: (reader: DirectoryUser) => personalWorkClient.grantPersonalWorkShare({ workId, readerUserId: reader.userId }),
|
|
83
|
+
onSuccess: () => { void client.invalidateQueries({ queryKey: agentWorkQueryKey(workId) }); },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function useRevokeWorkShare(workId: string) {
|
|
88
|
+
const client = useQueryClient();
|
|
89
|
+
return useMutation({
|
|
90
|
+
mutationFn: (readerUserId: string) => agentWorkClient.revokeWorkShare({ workId, readerUserId }),
|
|
91
|
+
onSuccess: () => { void client.invalidateQueries({ queryKey: agentWorkQueryKey(workId) }); },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type LibraryFilter = { name: string; workId: string; addedWithinDays: number | null };
|
|
96
|
+
|
|
97
|
+
export type Library = { files: LibraryFile[]; works: LibraryWork[] };
|
|
98
|
+
|
|
99
|
+
/** The person's Personal Library, narrowed as asked, most recently added first. */
|
|
100
|
+
export function useLibrary(filter: LibraryFilter) {
|
|
101
|
+
const userId = useCurrentUser().data?.user?.id;
|
|
102
|
+
return useQuery({
|
|
103
|
+
queryKey: ["personal-library", userId ?? "", filter.name.trim(), filter.workId, filter.addedWithinDays],
|
|
104
|
+
queryFn: async ({ signal }): Promise<Library> => {
|
|
105
|
+
const after = filter.addedWithinDays === null ? undefined : timestampFromDate(new Date(Date.now() - filter.addedWithinDays * 86_400_000));
|
|
106
|
+
const response = await personalWorkClient.listLibraryFiles({ nameQuery: filter.name.trim(), workId: filter.workId, addedAfter: after }, { signal });
|
|
107
|
+
return { files: response.files, works: response.works };
|
|
108
|
+
},
|
|
109
|
+
enabled: Boolean(userId),
|
|
110
|
+
retry: false,
|
|
111
|
+
placeholderData: keepPreviousData,
|
|
112
|
+
refetchOnWindowFocus: false,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** What a new Personal Work may start with, under the person's own Agent. */
|
|
117
|
+
export const personalWorkStartSource: Omit<WorkStartSource, "draft"> = {
|
|
118
|
+
key: ["personal"],
|
|
119
|
+
loadOptions: async signal => {
|
|
120
|
+
const options = (await personalWorkClient.getPersonalWorkStartOptions({}, { signal })).options;
|
|
121
|
+
return { models: options?.models ?? [], plugins: options?.plugins ?? [], connectors: options?.connectors ?? [] };
|
|
122
|
+
},
|
|
123
|
+
loadRepositories: async signal => {
|
|
124
|
+
const found = (await personalWorkClient.listPersonalWorkStartRepositories({}, { signal })).repositories;
|
|
125
|
+
return { repositories: found?.repositories ?? [], recentRepositoryIds: found?.recentRepositoryIds ?? [], repositoriesUnavailable: found?.repositoriesUnavailable ?? false };
|
|
126
|
+
},
|
|
127
|
+
chooseComputer: true,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Starts a Personal Work. One attempt keeps its request identity until it
|
|
131
|
+
* succeeds, so a retried start returns the Work it already started. */
|
|
132
|
+
export function useStartPersonalWork() {
|
|
133
|
+
const client = useQueryClient();
|
|
134
|
+
const userId = useCurrentUser().data?.user?.id;
|
|
135
|
+
const attempt = useRef<string | null>(null);
|
|
136
|
+
return useMutation({
|
|
137
|
+
mutationFn: async (input: WorkStartInput) => {
|
|
138
|
+
attempt.current ??= crypto.randomUUID();
|
|
139
|
+
const workId = (await personalWorkClient.startPersonalWork({ requestId: attempt.current, ...workStartFields(input) })).workId;
|
|
140
|
+
if (!workId) throw new Error("Work was not created.");
|
|
141
|
+
attempt.current = null;
|
|
142
|
+
return workId;
|
|
143
|
+
},
|
|
144
|
+
onSuccess: () => { void client.invalidateQueries({ queryKey: personalWorksQueryKey(userId) }); },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A Work the person reads in their Library: their own Personal Work or one
|
|
149
|
+
* shared with them; undefined until known or when the Library has none. */
|
|
150
|
+
export function useLibraryWork(workId: string | undefined, enabled = true) {
|
|
151
|
+
return useQuery({
|
|
152
|
+
queryKey: ["library-work", workId],
|
|
153
|
+
queryFn: async ({ signal }) => (await personalWorkClient.listLibraryFiles({ workId }, { signal })).works.find(work => work.workId === workId) ?? null,
|
|
154
|
+
enabled: Boolean(workId) && enabled,
|
|
155
|
+
retry: false,
|
|
156
|
+
refetchOnWindowFocus: false,
|
|
157
|
+
staleTime: 60_000,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { JsonObject, JsonValue } from "@bufbuild/protobuf";
|
|
2
|
+
import type { InstalledPlugin, ParameterDeclaration } from "@talgent/workbench-contracts/marketplace/public/v1/marketplace_pb";
|
|
3
|
+
|
|
4
|
+
// Form values are text as typed; they become typed manual values only after
|
|
5
|
+
// validation against the release's declarations. Blank means "not set here",
|
|
6
|
+
// so a declared default or a saved value continues to apply.
|
|
7
|
+
export type ParameterDrafts = Record<string, string>;
|
|
8
|
+
export type ParameterErrors = Record<string, string>;
|
|
9
|
+
|
|
10
|
+
export const hasDefault = (declaration: ParameterDeclaration) => declaration.hasDefault || declaration.default !== undefined;
|
|
11
|
+
export const parameterLabel = (name: string, declaration: ParameterDeclaration) => declaration.title || name;
|
|
12
|
+
|
|
13
|
+
// A value that must be supplied: required, no declared default and nothing saved.
|
|
14
|
+
export function missingRequired(declarations: Record<string, ParameterDeclaration>, saved: { configuration?: JsonObject; redacted?: string[] }, drafts: ParameterDrafts = {}) {
|
|
15
|
+
return Object.entries(declarations).filter(([name, declaration]) => declaration.required && !hasDefault(declaration)
|
|
16
|
+
&& !drafts[name]?.trim() && saved.configuration?.[name] === undefined && !saved.redacted?.includes(name)).map(([name]) => name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Missing required settings on an existing installation, such as after a new
|
|
20
|
+
// release declares another required parameter.
|
|
21
|
+
export function installationNeedsSetup(declarations: Record<string, ParameterDeclaration>, installed: InstalledPlugin) {
|
|
22
|
+
return missingRequired(declarations, { configuration: installed.configuration, redacted: installed.redactedParameters }).length > 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseParameters(declarations: Record<string, ParameterDeclaration>, drafts: ParameterDrafts, saved: { configuration?: JsonObject; redacted?: string[] } = {}): { values: JsonObject; errors: ParameterErrors } {
|
|
26
|
+
const values: JsonObject = {};
|
|
27
|
+
const errors: ParameterErrors = {};
|
|
28
|
+
for (const name of missingRequired(declarations, saved, drafts)) errors[name] = `Enter ${parameterLabel(name, declarations[name]).toLowerCase()}.`;
|
|
29
|
+
for (const [name, declaration] of Object.entries(declarations)) {
|
|
30
|
+
const raw = drafts[name];
|
|
31
|
+
if (raw === undefined || !raw.trim() || errors[name]) continue;
|
|
32
|
+
const parsed = parseValue(declaration, raw);
|
|
33
|
+
if ("error" in parsed) errors[name] = parsed.error;
|
|
34
|
+
else values[name] = parsed.value;
|
|
35
|
+
}
|
|
36
|
+
return { values, errors };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseValue(declaration: ParameterDeclaration, raw: string): { value: JsonValue } | { error: string } {
|
|
40
|
+
if (declaration.multiple) {
|
|
41
|
+
const items = raw.split("\n").map(item => item.trim()).filter(Boolean);
|
|
42
|
+
return items.length ? { value: items } : { error: "Enter at least one value." };
|
|
43
|
+
}
|
|
44
|
+
if (declaration.type === "number") {
|
|
45
|
+
const number = Number(raw);
|
|
46
|
+
if (!Number.isFinite(number)) return { error: "Enter a number." };
|
|
47
|
+
if (declaration.min !== undefined && number < declaration.min) return { error: `Enter ${declaration.min} or more.` };
|
|
48
|
+
if (declaration.max !== undefined && number > declaration.max) return { error: `Enter ${declaration.max} or less.` };
|
|
49
|
+
return { value: number };
|
|
50
|
+
}
|
|
51
|
+
if (declaration.type === "boolean") return { value: raw === "true" };
|
|
52
|
+
if (declaration.options.length && !declaration.options.includes(raw)) return { error: "Choose one of the options." };
|
|
53
|
+
return { value: raw };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Saved values shown back in a form. Sensitive values are never returned by
|
|
57
|
+
// ordinary queries, so they stay blank with a "saved" placeholder.
|
|
58
|
+
export function draftsFrom(configuration: JsonObject | undefined): ParameterDrafts {
|
|
59
|
+
const drafts: ParameterDrafts = {};
|
|
60
|
+
for (const [name, value] of Object.entries(configuration ?? {})) {
|
|
61
|
+
drafts[name] = Array.isArray(value) ? value.join("\n") : String(value);
|
|
62
|
+
}
|
|
63
|
+
return drafts;
|
|
64
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { WorkFilePreviewDisplay as Display, WorkFilePreviewMode as Mode, WorkFilePreviewRefusal as Refusal } from "@talgent/workbench-contracts/agent/public/v1/work_file_pb";
|
|
2
|
+
import type { PreviewVerdict } from "../components/preview/preview-verdict";
|
|
3
|
+
|
|
4
|
+
// Storage's file preview as a context relays it. Agent's preview contract
|
|
5
|
+
// numbers its values as Storage does, and a context relaying it keeps that
|
|
6
|
+
// numbering, so one reading of Agent's enums serves every relay.
|
|
7
|
+
type RelayedPreview = { mode: number; display: number; refusal: number; limitBytes: bigint; accessUrl: string; rendering: boolean };
|
|
8
|
+
|
|
9
|
+
const previewModes: Partial<Record<number, PreviewVerdict["mode"]>> = {
|
|
10
|
+
[Mode.REFUSED]: "refused", [Mode.PREVIEW_COPY]: "preview_copy", [Mode.RENDER]: "render", [Mode.DIRECT]: "direct", [Mode.TRUNCATED]: "truncated",
|
|
11
|
+
};
|
|
12
|
+
const previewDisplays: Partial<Record<number, PreviewVerdict["display"]>> = { [Display.TEXT]: "text", [Display.PDF]: "pdf", [Display.IMAGE]: "image" };
|
|
13
|
+
const previewRefusals: Partial<Record<number, NonNullable<PreviewVerdict["refusal"]>>> = {
|
|
14
|
+
[Refusal.UNSUPPORTED]: "unsupported", [Refusal.STRUCTURE_MISMATCH]: "structure_mismatch", [Refusal.ENCRYPTED]: "encrypted", [Refusal.TOO_LARGE]: "too_large",
|
|
15
|
+
[Refusal.CORRUPT]: "corrupt", [Refusal.CONVERTER_REFUSED]: "converter_refused", [Refusal.RENDERING_TOO_LARGE]: "rendering_too_large",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// An unknown mode from a newer server reads as unsupported: nothing unknown is shown.
|
|
19
|
+
export function previewVerdict(preview: RelayedPreview | undefined): PreviewVerdict {
|
|
20
|
+
const mode = preview && previewModes[preview.mode];
|
|
21
|
+
if (!preview || !mode) return { mode: "refused", display: "none", refusal: "unsupported", limitBytes: 0, rendering: false };
|
|
22
|
+
return {
|
|
23
|
+
mode, display: previewDisplays[preview.display] ?? "none", refusal: previewRefusals[preview.refusal],
|
|
24
|
+
limitBytes: Number(preview.limitBytes), url: preview.accessUrl || undefined, rendering: preview.rendering,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** How long ago a moment was, in the workspace's compact form ("5m ago"). */
|
|
2
|
+
export function timeAgo(date: Date | null | undefined, now = Date.now()): string {
|
|
3
|
+
if (!date) return "just now";
|
|
4
|
+
const diffSeconds = Math.max(0, Math.floor((now - date.getTime()) / 1000));
|
|
5
|
+
if (diffSeconds < 60) return "just now";
|
|
6
|
+
const minutes = Math.floor(diffSeconds / 60);
|
|
7
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
8
|
+
const hours = Math.floor(minutes / 60);
|
|
9
|
+
if (hours < 24) return `${hours}h ago`;
|
|
10
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
11
|
+
}
|
package/lib/session.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// The browser side of Account's Login Session. The SPA never sees a token:
|
|
2
|
+
// Account sets HttpOnly cookies, and these calls only ask it to refresh or end
|
|
3
|
+
// the session of this device.
|
|
4
|
+
|
|
5
|
+
const jsonPost: RequestInit = {
|
|
6
|
+
method: "POST",
|
|
7
|
+
credentials: "same-origin",
|
|
8
|
+
headers: { "Content-Type": "application/json" },
|
|
9
|
+
body: "{}",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const maxNextLength = 2048;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* An internal Portal path, or "/" for anything that could leave the site or
|
|
16
|
+
* loop back into sign-in. Mirrors internalPath in Account's /auth handler,
|
|
17
|
+
* which re-checks it; this copy keeps the browser from building such links.
|
|
18
|
+
*/
|
|
19
|
+
export function safeInternalPath(value: string | null | undefined): string {
|
|
20
|
+
const path = value?.trim() ?? "";
|
|
21
|
+
if (!path.startsWith("/") || path.startsWith("//") || path.length > maxNextLength || /[\\\r\n\t]/.test(path)) return "/";
|
|
22
|
+
const pathname = path.split(/[?#]/, 1)[0];
|
|
23
|
+
if (pathname === "/login" || pathname === "/auth" || pathname.startsWith("/auth/")) return "/";
|
|
24
|
+
return path;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Where the "Continue with GitHub" action starts a sign-in. GitHub returns
|
|
28
|
+
* every sign-in to one entry's host, so the sign-in names this entry's origin
|
|
29
|
+
* to come back to; Account returns only to the entries it trusts. */
|
|
30
|
+
export function signInHref(next: string): string {
|
|
31
|
+
return `/auth/github/login?next=${encodeURIComponent(`${window.location.origin}${safeInternalPath(next)}`)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The Portal sign-in page, returning to next afterwards. home is where the
|
|
35
|
+
* entry lands after sign-in anyway, so naming it adds nothing. */
|
|
36
|
+
export function signInPagePath(next: string, home = "/"): string {
|
|
37
|
+
const target = safeInternalPath(next);
|
|
38
|
+
return target === "/" || target === home ? "/login" : `/login?next=${encodeURIComponent(target)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** refreshed: a new token is set. rejected: sign in again. unavailable: try later. */
|
|
42
|
+
export type RefreshOutcome = "refreshed" | "rejected" | "unavailable";
|
|
43
|
+
|
|
44
|
+
let inFlight: Promise<RefreshOutcome> | undefined;
|
|
45
|
+
|
|
46
|
+
// Once Account rejects a refresh, this device's Login Session is over: Account
|
|
47
|
+
// has cleared its cookies, and only signing in again, which reloads the page,
|
|
48
|
+
// starts another. Pages learn it at once instead of at their next session
|
|
49
|
+
// check, and no call asks Account again meanwhile.
|
|
50
|
+
let ended = false;
|
|
51
|
+
const endListeners = new Set<() => void>();
|
|
52
|
+
|
|
53
|
+
export function sessionEnded(): boolean {
|
|
54
|
+
return ended;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function subscribeSessionEnd(listener: () => void): () => void {
|
|
58
|
+
endListeners.add(listener);
|
|
59
|
+
return () => endListeners.delete(listener);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function endSession() {
|
|
63
|
+
if (ended) return;
|
|
64
|
+
ended = true;
|
|
65
|
+
for (const listener of endListeners) listener();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function requestRefresh(): Promise<RefreshOutcome> {
|
|
69
|
+
if (ended) return "rejected";
|
|
70
|
+
let response: Response;
|
|
71
|
+
try {
|
|
72
|
+
response = await fetch("/auth/refresh", jsonPost);
|
|
73
|
+
} catch {
|
|
74
|
+
return "unavailable";
|
|
75
|
+
}
|
|
76
|
+
if (response.ok) return "refreshed";
|
|
77
|
+
if (response.status !== 401) return "unavailable";
|
|
78
|
+
endSession();
|
|
79
|
+
return "rejected";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Tabs share one refresh cookie. Account revokes a session when a replaced
|
|
83
|
+
// secret comes back, so two tabs must never present the same secret: the
|
|
84
|
+
// lock serializes them, and the second tab then sends the cookie the first
|
|
85
|
+
// one received.
|
|
86
|
+
async function refreshAcrossTabs(): Promise<RefreshOutcome> {
|
|
87
|
+
const locks = typeof navigator !== "undefined" ? navigator.locks : undefined;
|
|
88
|
+
if (!locks) return requestRefresh();
|
|
89
|
+
return locks.request("talgent-session-refresh", requestRefresh);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Refreshes this device's session once, however many callers ask at once. */
|
|
93
|
+
export function refreshSession(): Promise<RefreshOutcome> {
|
|
94
|
+
if (ended) return Promise.resolve("rejected");
|
|
95
|
+
inFlight ??= refreshAcrossTabs().finally(() => {
|
|
96
|
+
inFlight = undefined;
|
|
97
|
+
});
|
|
98
|
+
return inFlight;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Ends this device's Login Session; other devices stay signed in. Fails when
|
|
103
|
+
* Account could not revoke it, and the device then stays signed in.
|
|
104
|
+
*/
|
|
105
|
+
export async function signOut(): Promise<void> {
|
|
106
|
+
const response = await fetch("/auth/logout", jsonPost);
|
|
107
|
+
if (!response.ok) throw new Error(`sign-out failed with ${response.status}`);
|
|
108
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Each image serves only its own content-hashed assets, so after a release a
|
|
2
|
+
// page still running the previous build asks for chunks that no longer exist
|
|
3
|
+
// (the relation graph, Mermaid, a code language). Vite reports that failure as
|
|
4
|
+
// `vite:preloadError`; reloading picks up the current build. A failure right
|
|
5
|
+
// after such a reload is a real outage, not a stale page, so it is left to
|
|
6
|
+
// surface instead of reloading in a loop.
|
|
7
|
+
const storageKey = "talgent:stale-build-reload";
|
|
8
|
+
const retryAfterMs = 10_000;
|
|
9
|
+
|
|
10
|
+
type ReloadTarget = Pick<Window, "addEventListener" | "removeEventListener" | "sessionStorage"> & { location: Pick<Location, "reload"> };
|
|
11
|
+
|
|
12
|
+
export function installStaleBuildReload(target: ReloadTarget = window, now: () => number = Date.now) {
|
|
13
|
+
const onPreloadError = (event: Event) => {
|
|
14
|
+
const last = readLast(target);
|
|
15
|
+
if (last !== null && now() - last < retryAfterMs) return;
|
|
16
|
+
// Without storage a reload could not be told from the next one, so it could loop.
|
|
17
|
+
if (!writeLast(target, now())) return;
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
target.location.reload();
|
|
20
|
+
};
|
|
21
|
+
target.addEventListener("vite:preloadError", onPreloadError);
|
|
22
|
+
return () => target.removeEventListener("vite:preloadError", onPreloadError);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readLast(target: ReloadTarget): number | null {
|
|
26
|
+
try {
|
|
27
|
+
const value = Number(target.sessionStorage.getItem(storageKey));
|
|
28
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeLast(target: ReloadTarget, at: number) {
|
|
35
|
+
try {
|
|
36
|
+
target.sessionStorage.setItem(storageKey, String(at));
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { JsonObject } from "@bufbuild/protobuf";
|
|
2
|
+
import type { PermissionMode } from "@talgent/workbench-contracts/agent/public/v1/harness_pb";
|
|
3
|
+
|
|
4
|
+
/** What the person chose for a new Work, whatever the Work is for. */
|
|
5
|
+
export interface WorkStartInput {
|
|
6
|
+
initialMessage: string;
|
|
7
|
+
/** The selected Model Configuration, or the Managed Computer's own Claude
|
|
8
|
+
* login; absence leaves it to the platform's default. */
|
|
9
|
+
model?: { configurationId?: string; machineLogin?: boolean };
|
|
10
|
+
thinkingLevel?: string;
|
|
11
|
+
creationCapabilities?: {
|
|
12
|
+
plugins: Array<{ name: string; include: boolean; configuration: JsonObject }>;
|
|
13
|
+
connectors: Array<{ name: string; include: boolean }>;
|
|
14
|
+
startupAgent?: string;
|
|
15
|
+
};
|
|
16
|
+
repositorySelections?: Array<{ repositoryId: string; baseBranch: string }>;
|
|
17
|
+
/** The Computer the Work runs on for its life; absence leaves it to the platform's default Computer. */
|
|
18
|
+
computerId?: string;
|
|
19
|
+
/** How the Harness runs on a Managed Computer; absent for a Cloud. */
|
|
20
|
+
harness?: { permissionMode: PermissionMode; sandbox: boolean };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The start request's fields for the choices, leaving unchosen ones unset. */
|
|
24
|
+
export function workStartFields(input: WorkStartInput) {
|
|
25
|
+
return {
|
|
26
|
+
initialMessage: input.initialMessage,
|
|
27
|
+
repositorySelections: input.repositorySelections ?? [],
|
|
28
|
+
...(input.model && { model: input.model }),
|
|
29
|
+
...(input.thinkingLevel !== undefined && { thinkingLevel: input.thinkingLevel }),
|
|
30
|
+
...(input.creationCapabilities && { creationCapabilities: input.creationCapabilities }),
|
|
31
|
+
...(input.computerId && { computerId: input.computerId }),
|
|
32
|
+
...(input.harness && { harness: input.harness }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StartupProgress } from "@talgent/workbench-contracts/agent/public/v1/startup_pb";
|
|
2
|
+
|
|
3
|
+
// Revisions order committed Work projections, including completed progress.
|
|
4
|
+
// They do not establish native feedback ownership or ordering across processes.
|
|
5
|
+
export function newestStartupProgress(current: StartupProgress | undefined, incoming: StartupProgress | undefined) {
|
|
6
|
+
if (!incoming) return current;
|
|
7
|
+
if (!current || incoming.revision > current.revision) return incoming;
|
|
8
|
+
return current;
|
|
9
|
+
}
|
package/lib/subagents.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SubAgent as ProtoSubAgent } from "@talgent/workbench-contracts/agent/public/v1/subagents_pb";
|
|
2
|
+
|
|
3
|
+
// The SubAgents the main Agent of the Work's current Harness process delegated
|
|
4
|
+
// to, as the Work reports them; the page never derives a state itself.
|
|
5
|
+
export type SubAgentStatus = "running" | "completed" | "failed" | "stopped";
|
|
6
|
+
export interface SubAgent { id: string; type: string; task: string; status: SubAgentStatus }
|
|
7
|
+
|
|
8
|
+
const statuses: ReadonlySet<string> = new Set<SubAgentStatus>(["running", "completed", "failed", "stopped"]);
|
|
9
|
+
|
|
10
|
+
export function adaptSubAgents(items: ReadonlyArray<Pick<ProtoSubAgent, "id" | "type" | "task" | "status">> | undefined): SubAgent[] {
|
|
11
|
+
return (items ?? []).flatMap(item => item.id && statuses.has(item.status) ? [{ id: item.id, type: item.type, task: item.task, status: item.status as SubAgentStatus }] : []);
|
|
12
|
+
}
|