@talgent/workbench-ui 0.0.0 → 0.1.2

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 (205) hide show
  1. package/README.md +15 -2
  2. package/assets/brand-icons/LICENSE.md +30 -0
  3. package/assets/brand-icons/README.md +5 -0
  4. package/assets/brand-icons/figma.svg +1 -0
  5. package/assets/brand-icons/github.svg +1 -0
  6. package/assets/brand-icons/linear.svg +1 -0
  7. package/assets/brand-icons/notion.svg +1 -0
  8. package/assets/brand-icons/sentry.svg +1 -0
  9. package/assets/brand-icons/slack.svg +1 -0
  10. package/assets/fonts/Inter-OFL.txt +93 -0
  11. package/assets/fonts/inter-latin.woff2 +0 -0
  12. package/components/agent/agent-icon.css +14 -0
  13. package/components/agent/agent-icon.tsx +14 -0
  14. package/components/analytics/chart-geometry.ts +95 -0
  15. package/components/analytics/dashboard-charts.tsx +136 -0
  16. package/components/analytics/model-usage-chart.tsx +84 -0
  17. package/components/analytics/token-breakdown.css +13 -0
  18. package/components/analytics/token-breakdown.tsx +23 -0
  19. package/components/analytics/token-report.tsx +57 -0
  20. package/components/analytics/work-usage/turn-timeline.tsx +98 -0
  21. package/components/analytics/work-usage/turns.ts +170 -0
  22. package/components/analytics/work-usage/work-status.ts +36 -0
  23. package/components/analytics/work-usage/work-usage-reader.tsx +122 -0
  24. package/components/analytics/work-usage/work-usage.css +48 -0
  25. package/components/auth/auth-gate.tsx +44 -0
  26. package/components/chat/message-input.tsx +105 -0
  27. package/components/composer/permission-mode-list.tsx +15 -0
  28. package/components/composer/prompt-bar-controls.tsx +77 -0
  29. package/components/composer/prompt-bar.tsx +45 -0
  30. package/components/composer/skill-completion.css +27 -0
  31. package/components/composer/skill-completion.tsx +49 -0
  32. package/components/composer/use-slash-completion.tsx +69 -0
  33. package/components/composer/work-start.css +133 -0
  34. package/components/feedback/toast-region.css +2 -0
  35. package/components/feedback/toast-region.tsx +15 -0
  36. package/components/files/file-facts.tsx +16 -0
  37. package/components/files/file-section.tsx +181 -0
  38. package/components/markdown.tsx +144 -0
  39. package/components/marketplace/connector-pages.tsx +166 -0
  40. package/components/marketplace/marketplace-icons.tsx +66 -0
  41. package/components/marketplace/marketplace-shared.tsx +43 -0
  42. package/components/marketplace/marketplace-shell.tsx +32 -0
  43. package/components/marketplace/marketplace.css +97 -0
  44. package/components/marketplace/parameter-fields.tsx +46 -0
  45. package/components/marketplace/plugin-list.tsx +84 -0
  46. package/components/marketplace/plugin-page.tsx +133 -0
  47. package/components/marketplace/provider-pages.tsx +120 -0
  48. package/components/preview/file-format.ts +75 -0
  49. package/components/preview/file-preview.tsx +153 -0
  50. package/components/preview/preview-verdict.ts +44 -0
  51. package/components/preview/preview.css +19 -0
  52. package/components/preview/text-preview.tsx +174 -0
  53. package/components/profile/provider-callback.tsx +43 -0
  54. package/components/profile/work-agent-appearance.tsx +109 -0
  55. package/components/profile/work-agent-soul.tsx +76 -0
  56. package/components/rail/rail-section.tsx +20 -0
  57. package/components/rail/rail.css +19 -0
  58. package/components/rail/use-expand-on-change.ts +18 -0
  59. package/components/session/startup-progress.css +77 -0
  60. package/components/session/startup-progress.tsx +103 -0
  61. package/components/session/work-permission-control.tsx +45 -0
  62. package/components/session/work-thinking-control.tsx +52 -0
  63. package/components/shell/account-menu.tsx +134 -0
  64. package/components/shell/app-shell.tsx +148 -0
  65. package/components/shell/core-reading-views.tsx +26 -0
  66. package/components/shell/core-reading.tsx +17 -0
  67. package/components/shell/document-title.tsx +11 -0
  68. package/components/shell/entry-shell.ts +39 -0
  69. package/components/shell/file-reading.tsx +22 -0
  70. package/components/shell/page-layout.tsx +7 -0
  71. package/components/shell/reading-context.ts +41 -0
  72. package/components/shell/reading-panel.css +67 -0
  73. package/components/shell/reading-panel.tsx +74 -0
  74. package/components/shell/sidebar-parts.tsx +38 -0
  75. package/components/work/context-rail.css +61 -0
  76. package/components/work/conversation-unavailable.css +6 -0
  77. package/components/work/conversation-unavailable.tsx +11 -0
  78. package/components/work/subagent-item.tsx +42 -0
  79. package/components/work/subagents-section.css +31 -0
  80. package/components/work/subagents-section.tsx +40 -0
  81. package/components/work/token-usage.tsx +28 -0
  82. package/components/work/tool-message-detail.tsx +39 -0
  83. package/components/work/tool-output-preview.tsx +52 -0
  84. package/components/work/tool-preview-reading.tsx +27 -0
  85. package/components/work/tool-reading-content.tsx +31 -0
  86. package/components/work/work-context-rail.tsx +23 -0
  87. package/components/work/work-conversation.tsx +207 -0
  88. package/components/work/work-history.tsx +115 -0
  89. package/components/work/work-interaction.css +38 -0
  90. package/components/work/work-interaction.tsx +105 -0
  91. package/components/work/work-message-flow.css +86 -0
  92. package/components/work/work-message-flow.tsx +352 -0
  93. package/components/work/work-progress-section.tsx +20 -0
  94. package/components/work/work-prompt-bar.tsx +82 -0
  95. package/components/work/work-question-record.tsx +72 -0
  96. package/components/work/work-snapshot-state.tsx +24 -0
  97. package/components/work/work-status-control.tsx +54 -0
  98. package/components/work-start/start-work-launch-state.ts +33 -0
  99. package/components/work-start/work-start-catalog.tsx +116 -0
  100. package/components/work-start/work-start-form.tsx +173 -0
  101. package/components/work-start/work-start-tray.css +40 -0
  102. package/components/work-start/work-start-tray.tsx +128 -0
  103. package/hooks/use-debounced.ts +11 -0
  104. package/hooks/use-work-conversation.ts +125 -0
  105. package/hooks/use-work-lifecycle-controls.ts +40 -0
  106. package/hooks/use-work-stream.ts +91 -0
  107. package/lib/account-service.ts +77 -0
  108. package/lib/agent-appearance-service.ts +59 -0
  109. package/lib/agent-icon.ts +26 -0
  110. package/lib/agent-soul-service.ts +48 -0
  111. package/lib/agent-work-service.ts +23 -0
  112. package/lib/client.ts +20 -0
  113. package/lib/computer-release.ts +30 -0
  114. package/lib/computer-service.ts +241 -0
  115. package/lib/dashboard-report.ts +38 -0
  116. package/lib/dashboard.ts +21 -0
  117. package/lib/marketplace-catalog.ts +138 -0
  118. package/lib/marketplace-connections.ts +98 -0
  119. package/lib/marketplace-plugins.ts +53 -0
  120. package/lib/marketplace-routes.ts +12 -0
  121. package/lib/model-vendors.ts +13 -0
  122. package/lib/name-taken-error.ts +7 -0
  123. package/lib/notifications.ts +37 -0
  124. package/lib/permission-modes.ts +25 -0
  125. package/lib/personal-usage-service.ts +19 -0
  126. package/lib/personal-work-service.ts +159 -0
  127. package/lib/plugin-parameters.ts +64 -0
  128. package/lib/preview-verdict.ts +26 -0
  129. package/lib/relative-time.ts +11 -0
  130. package/lib/session.ts +108 -0
  131. package/lib/share-link.ts +4 -0
  132. package/lib/stale-build-reload.ts +41 -0
  133. package/lib/start-work-request.ts +34 -0
  134. package/lib/startup-progress.ts +9 -0
  135. package/lib/subagents.ts +12 -0
  136. package/lib/thinking-levels.ts +2 -0
  137. package/lib/token-analytics.ts +168 -0
  138. package/lib/token-usage.ts +64 -0
  139. package/lib/trace-view-model.ts +292 -0
  140. package/lib/transport.ts +29 -0
  141. package/lib/use-now.ts +11 -0
  142. package/lib/use-work-trace.ts +175 -0
  143. package/lib/user-display.ts +49 -0
  144. package/lib/work-conversation.ts +118 -0
  145. package/lib/work-file-service.ts +118 -0
  146. package/lib/work-model.ts +38 -0
  147. package/lib/work-notices.ts +23 -0
  148. package/lib/work-progress.ts +41 -0
  149. package/lib/work-projection.ts +122 -0
  150. package/lib/work-questions.ts +18 -0
  151. package/lib/work-start-draft.ts +61 -0
  152. package/lib/work-start-loadout.ts +100 -0
  153. package/lib/work-start-source.ts +16 -0
  154. package/lib/work-state-presentation.ts +80 -0
  155. package/lib/work-stream-source.ts +32 -0
  156. package/package.json +81 -5
  157. package/pages/computers/computers-page.tsx +56 -0
  158. package/pages/computers/connect-computer-page.tsx +92 -0
  159. package/pages/computers/install-commands.tsx +42 -0
  160. package/pages/computers/remove-computer-dialog.tsx +33 -0
  161. package/pages/computers/rename-computer-dialog.tsx +37 -0
  162. package/pages/computers/room-rules-dialog.tsx +55 -0
  163. package/pages/login-page.css +33 -0
  164. package/pages/login-page.tsx +88 -0
  165. package/pages/login-threads.tsx +51 -0
  166. package/pages/marketplace-page.tsx +59 -0
  167. package/pages/profile-page.tsx +68 -0
  168. package/pages/workbench/library-page.tsx +88 -0
  169. package/pages/workbench/new-work-page.tsx +24 -0
  170. package/pages/workbench/person.tsx +13 -0
  171. package/pages/workbench/personal-work-context.tsx +93 -0
  172. package/pages/workbench/personal-work-page.tsx +100 -0
  173. package/pages/workbench/personal-works-page.tsx +36 -0
  174. package/pages/workbench/share-dialog.tsx +57 -0
  175. package/pages/workbench/title-dialog.tsx +35 -0
  176. package/pages/workbench/usage-page.tsx +52 -0
  177. package/public/assets/brand/talgent-lettering.png +0 -0
  178. package/public/assets/brand/talgent-logo-page-bg.png +0 -0
  179. package/public/assets/brand/talgent-logo-white-bg.png +0 -0
  180. package/public/assets/brand/talgent-mark-page-bg.png +0 -0
  181. package/public/assets/brand/talgent-mark-white-bg.png +0 -0
  182. package/public/assets/brand/talgent-mark.png +0 -0
  183. package/public/assets/product-icons/LICENSE +21 -0
  184. package/public/assets/product-icons/README.md +13 -0
  185. package/public/assets/product-icons/anthropic.svg +1 -0
  186. package/public/assets/product-icons/deepseek.svg +1 -0
  187. package/public/assets/product-icons/glm.svg +1 -0
  188. package/public/assets/product-icons/kimi.svg +1 -0
  189. package/public/assets/product-icons/linear.svg +1 -0
  190. package/public/assets/product-icons/minimax.svg +1 -0
  191. package/public/assets/product-icons/notion.svg +1 -0
  192. package/public/assets/product-icons/openai.svg +1 -0
  193. package/public/assets/product-icons/qwen.svg +1 -0
  194. package/styles/base.css +95 -0
  195. package/styles/fonts.css +14 -0
  196. package/styles/markdown.css +79 -0
  197. package/styles/uber-theme.css +96 -0
  198. package/styles/work-composer.css +222 -0
  199. package/styles/work-stream.css +343 -0
  200. package/styles/workbench.css +144 -0
  201. package/styles/workspace.css +260 -0
  202. package/test-support/backend.ts +67 -0
  203. package/test-support/plugin-summary.ts +8 -0
  204. package/test-support/setup.ts +59 -0
  205. package/vite-preset.ts +85 -0
@@ -0,0 +1,122 @@
1
+ import { Code, ConnectError } from "@connectrpc/connect";
2
+ import type { StartupProgress } from "@talgent/workbench-contracts/agent/public/v1/startup_pb";
3
+ import { isTerminalWorkLifecycle } from "./work-state-presentation";
4
+ import { newestStartupProgress } from "./startup-progress";
5
+
6
+ // The Work aggregate is the authority; the page shows a projection of it built
7
+ // from GetWork snapshots and live frames. Frames travel without delivery
8
+ // guarantees, so a delayed one must never replace a newer state.
9
+
10
+ export interface LiveWorkLifecycle {
11
+ agentWorkId: string | null;
12
+ lifecycle: string;
13
+ updatedAt?: Date;
14
+ generation: number;
15
+ startupProgress?: StartupProgress;
16
+ }
17
+
18
+ export interface LiveWorkFrame {
19
+ lifecycle: string;
20
+ updatedAt?: Date;
21
+ startupProgress?: StartupProgress;
22
+ }
23
+
24
+ /** Applies a live frame for agentWorkId. A frame older than the live state
25
+ * already shown keeps that state; startup progress keeps its own revision order. */
26
+ export function nextLiveWorkLifecycle(state: LiveWorkLifecycle, agentWorkId: string | null, frame: LiveWorkFrame): LiveWorkLifecycle {
27
+ const sameWork = state.agentWorkId === agentWorkId;
28
+ const startupProgress = newestStartupProgress(sameWork ? state.startupProgress : undefined, frame.startupProgress);
29
+ if (sameWork && frame.updatedAt && state.updatedAt && frame.updatedAt.getTime() < state.updatedAt.getTime()) {
30
+ return startupProgress === state.startupProgress ? state : { ...state, startupProgress };
31
+ }
32
+ return {
33
+ agentWorkId,
34
+ lifecycle: frame.lifecycle,
35
+ startupProgress,
36
+ updatedAt: frame.updatedAt ?? (sameWork ? state.updatedAt : undefined),
37
+ generation: sameWork && !isTerminalWorkLifecycle(frame.lifecycle)
38
+ ? state.generation + 1
39
+ : sameWork ? state.generation : 0,
40
+ };
41
+ }
42
+
43
+ export type WorkSnapshotFreshness =
44
+ | { status: "current" }
45
+ | { status: "not_found" }
46
+ | { status: "denied" }
47
+ | { status: "unavailable" }
48
+ | { status: "stale"; lastUpdatedAt: Date };
49
+
50
+ interface SnapshotQueryState {
51
+ data: unknown;
52
+ error: unknown;
53
+ dataUpdatedAt: number;
54
+ }
55
+
56
+ /** Classifies the snapshot query behind the page. A refresh that fails after
57
+ * a successful read keeps the last state and marks it stale; it never changes
58
+ * the Work's lifecycle. Only an authoritative reader of the Work itself
59
+ * (GetWork) answering NotFound means the Work does not exist. */
60
+ export function workSnapshotFreshness(query: SnapshotQueryState, authoritative = true): WorkSnapshotFreshness {
61
+ if (!query.error) return { status: "current" };
62
+ const code = ConnectError.from(query.error).code;
63
+ if (authoritative && code === Code.NotFound) return { status: "not_found" };
64
+ if (query.data === undefined) return code === Code.PermissionDenied ? { status: "denied" } : { status: "unavailable" };
65
+ return { status: "stale", lastUpdatedAt: new Date(query.dataUpdatedAt) };
66
+ }
67
+
68
+ /** Work controls carry the version the caller observed; Agent answers Aborted
69
+ * when the Work has changed since, so nothing is applied to a state the user
70
+ * did not see. */
71
+ export function isStaleWorkControl(cause: unknown) {
72
+ return ConnectError.from(cause).code === Code.Aborted;
73
+ }
74
+
75
+ export const staleWorkControlMessage = "This Work changed since you last saw it. Review its current state and try again.";
76
+
77
+ export const withdrawnModelMessage = "This Work's model is no longer offered, so it can't resume. Ask your administrator to restore the model, then try again.";
78
+
79
+ // Agent refuses Resume and Restart of a Work whose Model Configuration is withdrawn.
80
+ function isWithdrawnModel(cause: unknown) {
81
+ const error = ConnectError.from(cause);
82
+ return error.code === Code.FailedPrecondition && error.rawMessage.includes("model configuration is withdrawn");
83
+ }
84
+
85
+ export const turnRunningMessage = "This Work is in the middle of a turn. Cancel the turn, then stop the Work.";
86
+
87
+ // Agent stops a Work only between Turns.
88
+ export function isTurnRunning(cause: unknown) {
89
+ const error = ConnectError.from(cause);
90
+ return error.code === Code.FailedPrecondition && error.rawMessage.includes("a Turn is running");
91
+ }
92
+
93
+ export const inputPendingMessage = "This Work still has messages to finish. Try again once they are handled.";
94
+
95
+ // Agent closes a Work only once its accepted input is finished.
96
+ export function isInputPending(cause: unknown) {
97
+ const error = ConnectError.from(cause);
98
+ return error.code === Code.FailedPrecondition && error.rawMessage.includes("still has accepted input to finish");
99
+ }
100
+
101
+ export const computerOfflineMessage = "This Work's computer is offline. Try again once it reconnects.";
102
+
103
+ // Agent refuses a start while the Work's Managed Computer is unreachable.
104
+ function isComputerUnreachable(cause: unknown) {
105
+ const error = ConnectError.from(cause);
106
+ return error.code === Code.FailedPrecondition && error.rawMessage.includes("the Computer is unreachable");
107
+ }
108
+
109
+ /** What a Work whose Resume is owed on reconnection says while its Managed
110
+ * Computer is away: the messages sent meanwhile wait for the machine. */
111
+ export function awaitingReconnectionNotice(computerName: string | undefined) {
112
+ return `${computerName || "This Work's computer"} is offline. Your messages are kept, and the Work resumes with them when it reconnects.`;
113
+ }
114
+
115
+ export function workControlErrorMessage(cause: unknown) {
116
+ if (isStaleWorkControl(cause)) return staleWorkControlMessage;
117
+ if (isComputerUnreachable(cause)) return computerOfflineMessage;
118
+ if (isWithdrawnModel(cause)) return withdrawnModelMessage;
119
+ if (isTurnRunning(cause)) return turnRunningMessage;
120
+ if (isInputPending(cause)) return inputPendingMessage;
121
+ return cause instanceof Error ? cause.message : "Work action failed.";
122
+ }
@@ -0,0 +1,18 @@
1
+ export interface WorkQuestion {
2
+ question: string;
3
+ options?: { label: string; description?: string }[];
4
+ multiSelect?: boolean;
5
+ }
6
+
7
+ export function parseWorkQuestions(json: string): { input: Record<string, unknown>; questions: WorkQuestion[] } | null {
8
+ try {
9
+ const input = JSON.parse(json);
10
+ if (!input || typeof input !== "object" || !Array.isArray(input.questions) || !input.questions.length) return null;
11
+ if (!input.questions.every((question: unknown) => {
12
+ if (!question || typeof question !== "object" || !("question" in question) || typeof question.question !== "string") return false;
13
+ if (!("options" in question)) return true;
14
+ return Array.isArray(question.options) && question.options.every(option => option && typeof option === "object" && typeof option.label === "string");
15
+ })) return null;
16
+ return { input, questions: input.questions };
17
+ } catch { return null; }
18
+ }
@@ -0,0 +1,61 @@
1
+ import { skipToken, useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import { useState } from "react";
3
+ import type { ParameterDrafts } from "./plugin-parameters";
4
+ import type { WorkStartInput } from "./start-work-request";
5
+
6
+ // Per-Work choices recorded as differences from the personal defaults. None of
7
+ // them is ever written back to a personal installation.
8
+ export interface WorkPluginChoice {
9
+ /** Undefined keeps the default inclusion; true adds, false removes. */
10
+ include?: boolean;
11
+ values: ParameterDrafts;
12
+ }
13
+
14
+ export interface WorkStartDraft {
15
+ instruction: string;
16
+ model?: WorkStartInput["model"];
17
+ thinkingLevel?: string;
18
+ repositories: string[];
19
+ plugins: Record<string, WorkPluginChoice>;
20
+ /** Per-Work Connector inclusion that differs from the default. */
21
+ connectors: Record<string, boolean>;
22
+ agent: string;
23
+ /** The chosen Computer; empty keeps the default, the first Cloud. */
24
+ computer: string;
25
+ /** The Harness choices for a Managed Computer; unset keeps their defaults. */
26
+ harness?: WorkStartInput["harness"];
27
+ files: Array<{ id: string; file: File; uploaded: boolean }>;
28
+ submitting: boolean;
29
+ }
30
+
31
+ /** What a draft is for: one per `key`, starting from `instruction`. */
32
+ export type WorkStartDraftScope = { key: string; instruction: string };
33
+
34
+ // The session QueryClient is cleared on logout. Files remain in memory only;
35
+ // every entry point of one scope observes the same draft and in-flight
36
+ // submission guard.
37
+ export function useWorkStartDraft(scope: WorkStartDraftScope) {
38
+ const client = useQueryClient();
39
+ const key = ["work-start-draft", scope.key] as const;
40
+ const { data } = useQuery<WorkStartDraft>({
41
+ queryKey: key,
42
+ queryFn: skipToken,
43
+ initialData: () => ({ instruction: scope.instruction, repositories: [], plugins: {}, connectors: {}, agent: "", computer: "", files: [], submitting: false }),
44
+ enabled: false,
45
+ staleTime: Infinity,
46
+ gcTime: Infinity,
47
+ structuralSharing: false,
48
+ });
49
+ // The cache notifies observers on a later tick, but a controlled textarea must
50
+ // take each keystroke at once: a value React restores even briefly ends an
51
+ // input method's composition and commits the raw keystrokes. The instruction is
52
+ // therefore held locally as well, and follows the cache when it changes elsewhere.
53
+ const [instruction, setInstruction] = useState(data!.instruction);
54
+ const [observed, setObserved] = useState(data!.instruction);
55
+ if (data!.instruction !== observed) { setObserved(data!.instruction); setInstruction(data!.instruction); }
56
+ function update(change: Partial<WorkStartDraft> | ((draft: WorkStartDraft) => WorkStartDraft)) {
57
+ const next = client.setQueryData<WorkStartDraft>(key, previous => previous && (typeof change === "function" ? change(previous) : { ...previous, ...change }));
58
+ if (next) { setObserved(next.instruction); setInstruction(next.instruction); }
59
+ }
60
+ return { draft: { ...data!, instruction }, update, current: () => client.getQueryData<WorkStartDraft>(key)! };
61
+ }
@@ -0,0 +1,100 @@
1
+ import type { JsonObject } from "@bufbuild/protobuf";
2
+ import type { WorkStartConnector, WorkStartPlugin } from "@talgent/workbench-contracts/agent/public/v1/work_start_options_pb";
3
+ import type { Connector, InstalledPlugin, PluginRelease, PluginSummary } from "@talgent/workbench-contracts/marketplace/public/v1/marketplace_pb";
4
+ import { pluginDisplayName, pluginPresentation } from "./marketplace-plugins";
5
+ import { missingRequired, parseParameters, parameterLabel } from "./plugin-parameters";
6
+ import type { WorkStartDraft } from "./work-start-draft";
7
+
8
+ export type Difference = "required" | "default" | "added" | "removed";
9
+ export const differenceLabel: Record<Difference, string> = { required: "Always included", default: "Your default", added: "Added for this Work", removed: "Removed for this Work" };
10
+
11
+ export interface Capability { name: string; description: string; argumentHint: string; plugin: string }
12
+ export interface LoadoutRow {
13
+ slug: string;
14
+ name: string;
15
+ difference: Difference;
16
+ included: boolean;
17
+ release?: PluginSummary;
18
+ installed?: InstalledPlugin;
19
+ skills: Capability[];
20
+ agents: Capability[];
21
+ /** Settings changed for this Work only. */
22
+ edited: string[];
23
+ /** A blocking problem, such as a required setting nobody provided. */
24
+ problem: string;
25
+ }
26
+
27
+ // The Plugins of this Work: required ones, the starter's enabled defaults and
28
+ // per-Work additions, each marked by how it differs from the defaults. Agent
29
+ // reports the Skills and Agents of the defaults; an addition's come from its
30
+ // full release (`details`), since the catalog lists summaries.
31
+ export function pluginLoadout(options: WorkStartPlugin[], catalog: PluginSummary[], installed: Map<string, InstalledPlugin> | undefined, draft: WorkStartDraft, details: Map<string, PluginRelease> = new Map()): LoadoutRow[] {
32
+ const releases = new Map(catalog.map(release => [release.slug, release]));
33
+ const rows = new Map<string, LoadoutRow>();
34
+ const row = (slug: string, base: "required" | "default" | "added", option?: WorkStartPlugin): LoadoutRow => {
35
+ const release = releases.get(slug), choice = draft.plugins[slug], mine = installed?.get(slug);
36
+ const included = base === "required" || (base === "added" ? true : choice?.include !== false);
37
+ const name = release ? pluginPresentation(release).name : option?.displayName && option.displayName !== slug ? option.displayName : pluginDisplayName(slug);
38
+ const capability = (items: Array<{ name: string; description: string; argumentHint?: string }>) => items.map(item => ({ name: item.name, description: item.description, argumentHint: item.argumentHint ?? "", plugin: name }));
39
+ const edited = Object.entries(choice?.values ?? {}).filter(([, value]) => value.trim()).map(([key]) => key);
40
+ // Saved settings are unknown until the installations load; nothing is flagged before.
41
+ const missing = release && base !== "required" && installed ? missingRequired(release.parameters, { configuration: mine?.configuration, redacted: mine?.redactedParameters }, choice?.values) : [];
42
+ return {
43
+ slug, name, release, installed: mine, included, edited,
44
+ difference: base === "default" && !included ? "removed" : base,
45
+ skills: capability(option?.skills ?? details.get(slug)?.skills ?? []), agents: capability(option?.agents ?? details.get(slug)?.agents ?? []),
46
+ problem: included && missing.length ? `Needs ${missing.map(key => parameterLabel(key, release!.parameters[key]).toLowerCase()).join(", ")}` : "",
47
+ };
48
+ };
49
+ for (const option of options) rows.set(option.name, row(option.name, option.required ? "required" : "default", option));
50
+ for (const [slug, choice] of Object.entries(draft.plugins)) if (choice.include && !rows.has(slug)) rows.set(slug, row(slug, "added"));
51
+ return [...rows.values()];
52
+ }
53
+
54
+ // Explicit per-Work differences sent at creation. Parameter overrides are
55
+ // validated against the release; they never change personal settings.
56
+ export function pluginChoices(rows: LoadoutRow[], draft: WorkStartDraft) {
57
+ const plugins: Array<{ name: string; include: boolean; configuration: JsonObject }> = [];
58
+ const errors: Record<string, Record<string, string>> = {};
59
+ for (const row of rows) {
60
+ if (row.difference === "required") continue;
61
+ const choice = draft.plugins[row.slug];
62
+ if (!row.included) { plugins.push({ name: row.slug, include: false, configuration: {} }); continue; }
63
+ if (row.difference === "default" && !row.edited.length) continue;
64
+ const parsed = row.release ? parseParameters(row.release.parameters, choice?.values ?? {}, { configuration: row.installed?.configuration, redacted: row.installed?.redactedParameters }) : { values: {}, errors: {} };
65
+ if (Object.keys(parsed.errors).length) errors[row.slug] = parsed.errors;
66
+ plugins.push({ name: row.slug, include: true, configuration: parsed.values });
67
+ }
68
+ return { plugins, errors };
69
+ }
70
+
71
+ export interface ConnectorRow {
72
+ name: string;
73
+ difference: Difference;
74
+ included: boolean;
75
+ /** Marketplace presentation and Provider connection, when listed. */
76
+ connector?: Connector;
77
+ /** Left out of a Work on the chosen Managed Computer, as Agent reports;
78
+ * never included or sent. */
79
+ unavailable: boolean;
80
+ }
81
+
82
+ // Agent reports the default selection (required entries and enabled
83
+ // installations); this Work may remove defaults or add other offered entries.
84
+ // On a Managed Computer, Agent says which Connectors its Work leaves out.
85
+ export function connectorLoadout(options: WorkStartConnector[], catalog: Connector[], draft: WorkStartDraft, managed = false): ConnectorRow[] {
86
+ const presentation = new Map(catalog.map(connector => [connector.name, connector]));
87
+ return options.map(option => {
88
+ const base: Difference = option.required ? "required" : option.includedByDefault ? "default" : "added";
89
+ const connector = presentation.get(option.name);
90
+ const unavailable = managed && option.unavailableOnManaged;
91
+ const choice = draft.connectors[option.name];
92
+ const included = !unavailable && (base === "required" || (choice ?? base === "default"));
93
+ return { name: option.name, connector, included, unavailable, difference: base === "default" && !included && !unavailable ? "removed" : base };
94
+ });
95
+ }
96
+
97
+ export function connectorChoices(rows: ConnectorRow[]) {
98
+ return rows.filter(row => !row.unavailable && (row.difference === "removed" || (row.difference === "added" && row.included)))
99
+ .map(row => ({ name: row.name, include: row.included }));
100
+ }
@@ -0,0 +1,16 @@
1
+ import type { WorkModelOption } from "@talgent/workbench-contracts/agent/public/v1/model_pb";
2
+ import type { WorkStartConnector, WorkStartPlugin, WorkStartRepositories } from "@talgent/workbench-contracts/agent/public/v1/work_start_options_pb";
3
+ import type { WorkStartDraftScope } from "./work-start-draft";
4
+
5
+ /** Where a new Work's choices come from: the options and repositories offered
6
+ * to whoever the Work runs for, `key` naming them so a change reloads them.
7
+ * Without `uploadAttachment` the Work takes no attachments; with
8
+ * `chooseComputer` the person picks the Computer it runs on. */
9
+ export type WorkStartSource = {
10
+ draft: WorkStartDraftScope;
11
+ key: readonly unknown[];
12
+ loadOptions: (signal: AbortSignal) => Promise<{ models: WorkModelOption[]; plugins: WorkStartPlugin[]; connectors: WorkStartConnector[] }>;
13
+ loadRepositories: (signal: AbortSignal) => Promise<Pick<WorkStartRepositories, "repositories" | "recentRepositoryIds" | "repositoriesUnavailable">>;
14
+ uploadAttachment?: (file: File) => Promise<unknown>;
15
+ chooseComputer?: boolean;
16
+ };
@@ -0,0 +1,80 @@
1
+ import { Archive, Circle, CirclePause, CircleX, Clock3, LoaderCircle, Unplug, type LucideIcon } from "lucide-react";
2
+
3
+ export type WorkPresentation = { label: string; icon: LucideIcon; color: "default" | "accent" | "success" | "warning" | "danger"; description?: string };
4
+
5
+ export const workStatePresentation = {
6
+ created: { label: "Created", icon: Circle, color: "default" },
7
+ starting: { label: "Starting", icon: LoaderCircle, color: "accent" },
8
+ working: { label: "Working", icon: LoaderCircle, color: "success" },
9
+ stopping: { label: "Stopping", icon: CirclePause, color: "default" },
10
+ stopped: { label: "Stopped", icon: CirclePause, color: "default" },
11
+ failed: { label: "Failed", icon: CircleX, color: "danger" },
12
+ closing: { label: "Closing", icon: LoaderCircle, color: "default" },
13
+ closed: { label: "Closed", icon: Archive, color: "default" },
14
+ } as const;
15
+
16
+ /** A Working Work whose Turn awaits its owner's answer. Waiting is a fact
17
+ * beside the lifecycle state, never one of its values. */
18
+ export const waitingPresentation = { label: "Waiting for your answer", icon: Clock3, color: "warning" } as const;
19
+
20
+ /** How long a Working or Stopping Work may stay disconnected before it
21
+ * stops. Agent decides the stop; this only words it, so it follows
22
+ * Agent's bound if that changes. */
23
+ const disconnectionGraceMinutes = 5;
24
+
25
+ /** Why a Stopped Work stopped, where the reason needs saying. */
26
+ const stopReasonLabels: Record<string, string> = { disconnected: "Disconnected" };
27
+
28
+ /** The line a stopped conversation shows for its stop reason, if any. */
29
+ const stopReasonNotices: Record<string, string> = { disconnected: `Stopped after being disconnected for ${disconnectionGraceMinutes} minutes.` };
30
+
31
+ export function stopReasonNotice(state: string, stateReason: string | undefined): string | undefined {
32
+ return state === "stopped" ? stopReasonNotices[stateReason ?? ""] : undefined;
33
+ }
34
+
35
+ /** What happens to a Work in each state once its Disconnection lasts the
36
+ * grace: a Working or Stopping Work stops, a Closing one is closed. A start
37
+ * is not stopped for it. */
38
+ const disconnectionOutcome: Record<string, string> = { working: "stops", stopping: "stops", closing: "closes" };
39
+
40
+ function clockOf(at: Date): string {
41
+ return at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
42
+ }
43
+
44
+ /** What a Disconnection means for a Work in the given state: since when the
45
+ * platform has lost sight of where it runs (its Managed Computer, or its
46
+ * Cloud sandbox) and, counted from then, when the Work stops or closes
47
+ * unless it reconnects. */
48
+ export function disconnectionNote(state: string, since: Date, now: number = Date.now()): string {
49
+ const unreachable = `Disconnected since ${clockOf(since)}.`;
50
+ const outcome = disconnectionOutcome[state];
51
+ if (!outcome) return unreachable;
52
+ const at = new Date(since.getTime() + disconnectionGraceMinutes * 60_000);
53
+ const then = at.getTime() <= now ? `The Work ${outcome} now.` : `The Work ${outcome} at ${clockOf(at)} unless it reconnects by then.`;
54
+ return `${unreachable} ${then}`;
55
+ }
56
+
57
+ /** The accessible name of a Work's status: its label and, when it has one,
58
+ * what it means, so a screen reader hears what the tooltip shows. */
59
+ export function workStatusName(prefix: string, presentation: { label: string; description?: string }): string {
60
+ return presentation.description ? `${prefix}${presentation.label}. ${presentation.description}` : `${prefix}${presentation.label}`;
61
+ }
62
+
63
+ export type PresentedWork = { state: string; waiting?: boolean; stateReason?: string; disconnectedSince?: Date | null };
64
+
65
+ /** A Work's presentation: its Waiting Turn, otherwise its state, qualified by
66
+ * a Disconnection (a fact beside the state) or a stop reason. */
67
+ export function workPresentation(work: PresentedWork | undefined): WorkPresentation | undefined {
68
+ if (!work) return undefined;
69
+ const base: WorkPresentation | undefined = work.waiting ? waitingPresentation : workStatePresentation[work.state as keyof typeof workStatePresentation];
70
+ if (!base) return undefined;
71
+ if (work.disconnectedSince && !isTerminalWorkLifecycle(work.state)) {
72
+ return { label: `${base.label} · Disconnected`, icon: Unplug, color: "warning", description: disconnectionNote(work.state, work.disconnectedSince) };
73
+ }
74
+ const reason = work.waiting || work.state !== "stopped" ? undefined : stopReasonLabels[work.stateReason ?? ""];
75
+ return reason ? { ...base, label: `${base.label} · ${reason}` } : base;
76
+ }
77
+
78
+ export function isTerminalWorkLifecycle(lifecycle: string): boolean {
79
+ return lifecycle === "stopped" || lifecycle === "failed" || lifecycle === "closed";
80
+ }
@@ -0,0 +1,32 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { MessageInitShape } from "@bufbuild/protobuf";
3
+ import type { CallOptions } from "@connectrpc/connect";
4
+ import {
5
+ GetMessageDetailRequestSchema, GetToolDetailRequestSchema, ListMessagesBySessionRequestSchema, WatchStreamRequestSchema,
6
+ type GetMessageDetailResponse, type GetToolDetailResponse, type ListMessagesBySessionResponse, type WatchStreamResponse,
7
+ } from "@talgent/workbench-contracts/streamgateway/public/v1/streamgateway_pb";
8
+ import { streamClient } from "./client";
9
+
10
+ /** Where a Work's message stream is read from: Streamgateway for its owner
11
+ * and Work Share holders by default; an entry may supply another source. */
12
+ export interface WorkStreamSource {
13
+ watchStream(request: MessageInitShape<typeof WatchStreamRequestSchema>, options?: CallOptions): AsyncIterable<WatchStreamResponse>;
14
+ listMessagesBySession(request: MessageInitShape<typeof ListMessagesBySessionRequestSchema>, options?: CallOptions): Promise<ListMessagesBySessionResponse>;
15
+ getMessageDetail(request: MessageInitShape<typeof GetMessageDetailRequestSchema>, options?: CallOptions): Promise<GetMessageDetailResponse>;
16
+ getToolDetail(request: MessageInitShape<typeof GetToolDetailRequestSchema>, options?: CallOptions): Promise<GetToolDetailResponse>;
17
+ }
18
+
19
+ // Delegates per call so the client is resolved where a stream is read, not
20
+ // when this module loads.
21
+ export const streamGatewaySource: WorkStreamSource = {
22
+ watchStream: (request, options) => streamClient.watchStream(request, options),
23
+ listMessagesBySession: (request, options) => streamClient.listMessagesBySession(request, options),
24
+ getMessageDetail: (request, options) => streamClient.getMessageDetail(request, options),
25
+ getToolDetail: (request, options) => streamClient.getToolDetail(request, options),
26
+ };
27
+
28
+ export const WorkStreamSourceContext = createContext<WorkStreamSource>(streamGatewaySource);
29
+
30
+ export function useWorkStreamSource() {
31
+ return useContext(WorkStreamSourceContext);
32
+ }
package/package.json CHANGED
@@ -1,14 +1,90 @@
1
1
  {
2
2
  "name": "@talgent/workbench-ui",
3
- "version": "0.0.0",
4
- "description": "Placeholder: published from https://github.com/talgent-work/workbench",
3
+ "version": "0.1.2",
4
+ "description": "The Talgent Workbench's pages and components, published as TypeScript source for web apps built with Vite.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/talgent-work/workbench.git"
8
+ "url": "git+https://github.com/talgent-work/workbench.git",
9
+ "directory": "packages/workbench-ui"
9
10
  },
11
+ "type": "module",
10
12
  "publishConfig": {
11
- "registry": "https://registry.npmjs.org/",
12
13
  "access": "public"
14
+ },
15
+ "exports": {
16
+ "./assets/*": "./assets/*",
17
+ "./styles/*": "./styles/*",
18
+ "./components/*.css": "./components/*.css",
19
+ "./components/analytics/chart-geometry": "./components/analytics/chart-geometry.ts",
20
+ "./components/analytics/work-usage/turns": "./components/analytics/work-usage/turns.ts",
21
+ "./components/analytics/work-usage/work-status": "./components/analytics/work-usage/work-status.ts",
22
+ "./components/preview/file-format": "./components/preview/file-format.ts",
23
+ "./components/preview/preview-verdict": "./components/preview/preview-verdict.ts",
24
+ "./components/rail/use-expand-on-change": "./components/rail/use-expand-on-change.ts",
25
+ "./components/shell/entry-shell": "./components/shell/entry-shell.ts",
26
+ "./components/shell/reading-context": "./components/shell/reading-context.ts",
27
+ "./components/work-start/start-work-launch-state": "./components/work-start/start-work-launch-state.ts",
28
+ "./components/*": "./components/*.tsx",
29
+ "./hooks/*": "./hooks/*.ts",
30
+ "./lib/*": "./lib/*.ts",
31
+ "./pages/*": "./pages/*.tsx",
32
+ "./test-support/*": "./test-support/*.ts",
33
+ "./vite-preset": "./vite-preset.ts"
34
+ },
35
+ "files": [
36
+ "assets",
37
+ "components",
38
+ "hooks",
39
+ "lib",
40
+ "pages",
41
+ "public",
42
+ "styles",
43
+ "test-support",
44
+ "vite-preset.ts"
45
+ ],
46
+ "dependencies": {
47
+ "@bufbuild/protobuf": "^2.11.0",
48
+ "@connectrpc/connect": "^2.1.1",
49
+ "@connectrpc/connect-web": "^2.1.1",
50
+ "@heroui/react": "^3.1.0",
51
+ "@heroui/styles": "^3.1.0",
52
+ "@streamdown/code": "1.1.1",
53
+ "@streamdown/mermaid": "1.0.3",
54
+ "@tanstack/react-query": "^5.99.0",
55
+ "lucide-react": "^1.8.0",
56
+ "react": "^19.2.4",
57
+ "react-dom": "^19.2.4",
58
+ "react-router-dom": "^7.18.2",
59
+ "streamdown": "2.6.0",
60
+ "@talgent/workbench-contracts": "0.1.2"
61
+ },
62
+ "devDependencies": {
63
+ "@eslint/js": "^9.39.4",
64
+ "@tailwindcss/vite": "^4.2.2",
65
+ "@testing-library/jest-dom": "^6.9.1",
66
+ "@testing-library/react": "^16.3.2",
67
+ "@testing-library/user-event": "^14.6.1",
68
+ "@types/node": "^24.12.2",
69
+ "@types/react": "^19.2.14",
70
+ "@types/react-dom": "^19.2.3",
71
+ "@vitejs/plugin-react": "^6.0.1",
72
+ "eslint": "^9.39.4",
73
+ "eslint-plugin-react-hooks": "^7.0.1",
74
+ "eslint-plugin-react-refresh": "^0.5.2",
75
+ "globals": "^17.4.0",
76
+ "jsdom": "^29.0.2",
77
+ "tailwindcss": "^4.2.2",
78
+ "typescript": "~6.0.2",
79
+ "typescript-eslint": "^8.58.0",
80
+ "vite": "^8.1.6",
81
+ "vitest": "^4.1.12"
82
+ },
83
+ "scripts": {
84
+ "build": "tsc -b",
85
+ "typecheck": "tsc -b",
86
+ "lint": "eslint .",
87
+ "test": "vitest run",
88
+ "test:watch": "vitest"
13
89
  }
14
- }
90
+ }
@@ -0,0 +1,56 @@
1
+ import { Card } from "@heroui/react";
2
+ import { Monitor } from "lucide-react";
3
+ import { LoadState } from "../../components/marketplace/marketplace-shared";
4
+ import { PageLayout } from "../../components/shell/page-layout";
5
+ import { presenceLabel, roomLabel, useComputers, type ManagedComputer } from "../../lib/computer-service";
6
+ import { useNow } from "../../lib/use-now";
7
+ import { InstallCommands } from "./install-commands";
8
+ import { RemoveComputerButton } from "./remove-computer-dialog";
9
+ import { RenameComputerButton } from "./rename-computer-dialog";
10
+ import { RoomRulesButton } from "./room-rules-dialog";
11
+ import "../../components/marketplace/marketplace.css";
12
+
13
+ /** Computers: the person's Managed Computers with their Presence, and how to
14
+ * add another. A Cloud is not one of them and never shows here. */
15
+ export function ComputersPage() {
16
+ const computers = useComputers();
17
+ const list = computers.data ?? [];
18
+ return <PageLayout workspaceTitle="Computers">
19
+ <div className="mx-auto grid w-[min(640px,100%)] gap-8 py-6">
20
+ <section className="mk-section" aria-labelledby="cp-list">
21
+ <h3 id="cp-list" className="mk-section-title">Your computers{list.length > 0 && <span className="mk-count">{list.length}</span>}</h3>
22
+ <LoadState noun="your computers" isPending={computers.isPending} isError={computers.isError} hasData={Boolean(computers.data)} isFetching={computers.isFetching} onRetry={() => void computers.refetch()} />
23
+ {computers.data && (list.length === 0
24
+ ? <p className="text-[14px] text-muted">No computers yet. Install Talgent Computer on a machine to run Works there.</p>
25
+ : <ComputerList computers={list} />)}
26
+ </section>
27
+ <InstallCommands />
28
+ </div>
29
+ </PageLayout>;
30
+ }
31
+
32
+ function ComputerList({ computers }: { computers: ManagedComputer[] }) {
33
+ const now = useNow(30_000);
34
+ return <Card className="mk-group">
35
+ <ul className="mk-group-list" aria-label="Your computers">
36
+ {computers.map(computer => {
37
+ const online = computer.presence.kind === "online";
38
+ return <li key={computer.id} className="!items-center">
39
+ <Monitor size={18} className={online ? "shrink-0" : "shrink-0 text-muted"} aria-hidden="true" />
40
+ <span className="mk-row-text min-w-0 flex-1">
41
+ <strong className="truncate">{computer.name}</strong>
42
+ <span>
43
+ <span className={online ? "text-success" : undefined}>{presenceLabel(computer.presence, now)}</span>
44
+ {computer.talgentComputerVersion && <> · Talgent Computer {computer.talgentComputerVersion}</>}
45
+ {computer.claudeCodeVersion && <> · Claude Code {computer.claudeCodeVersion}</>}
46
+ </span>
47
+ <span>{roomLabel(computer)} · {computer.policy === "queue" ? "Queues runs when full" : "Rejects runs when full"}</span>
48
+ </span>
49
+ <RoomRulesButton computer={computer} />
50
+ <RenameComputerButton computer={computer} />
51
+ <RemoveComputerButton computer={computer} />
52
+ </li>;
53
+ })}
54
+ </ul>
55
+ </Card>;
56
+ }