@xpufx/paseo-top 0.4.4 → 0.4.5

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 (129) hide show
  1. package/OVERVIEW.md +17 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +94 -1
  6. package/client/pill.tsx +690 -614
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +1 -1
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +308 -276
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +9 -3
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +51 -18
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/{surface.tsx → lifecycle/surface.tsx} +6 -6
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +50 -58
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -234
  48. package/package.json +7 -4
  49. package/paseo-plugin.json +2 -0
  50. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  51. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  52. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  53. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  54. package/screenshots/paseo-top-dashboard.jpg +0 -0
  55. package/screenshots/paseo-top-general-view.jpg +0 -0
  56. package/server/resources.ts +98 -0
  57. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  58. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  61. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  62. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  63. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  64. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  65. package/server/vendor/permission-audit/index.ts +1 -0
  66. package/server/vendor/permission-audit/server.ts +839 -0
  67. package/shared/helper-version.ts +1 -1
  68. package/shared/multi-host.ts +153 -159
  69. package/shared/resources.ts +145 -0
  70. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  71. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  72. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  73. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  74. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  75. package/shared/vendor/permission-audit/filter.ts +61 -0
  76. package/shared/vendor/permission-audit/index.ts +2 -0
  77. package/shared/vendor/permission-audit/shared.ts +119 -0
  78. package/shared/version.ts +1 -1
  79. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  80. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  81. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  82. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  83. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  84. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  85. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  86. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  87. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  88. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  89. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  90. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  91. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  92. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  93. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  94. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  95. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  96. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
  97. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  98. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  99. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  100. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  101. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  102. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  103. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  104. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
  105. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  106. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  107. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  108. package/client/vendor/paseo-plugin-helper/index.ts +0 -30
  109. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  110. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  111. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  112. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
  113. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  114. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  115. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  117. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  118. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  119. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  120. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  121. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  122. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  123. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  124. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  125. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  126. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  127. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  128. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
@@ -1,766 +0,0 @@
1
- import React, { useState, useMemo } from "react";
2
- import { View, Text, Linking, StyleSheet } from "react-native";
3
- import { usePluginTheme } from "./theme/index";
4
- import { getClientHost } from "./host";
5
- import { useRpcQuery, useRpcMutation } from "./query";
6
- import { Button } from "./components/Button";
7
- import { Card } from "./components/Card";
8
- import { Badge } from "./components/Badge";
9
- import { Stack } from "./layout/Stack";
10
- import { Row } from "./layout/Row";
11
- import { KeyValue, KeyValueGroup } from "./components/KeyValue";
12
- import { CommandBox } from "./components/CommandBox";
13
- import { CopyButton } from "./components/CopyButton";
14
- import { CodeBlock } from "./components/CodeBlock";
15
- import { HighlightedText } from "./components/HighlightedText";
16
- import { Collapsible } from "./components/Collapsible";
17
- import { FormRow } from "./layout/FormRow";
18
- import { ActionBar } from "./layout/ActionBar";
19
- import { TextInput } from "./components/TextInput";
20
- import { EmptyState } from "./components/EmptyState";
21
- import { copyToClipboard } from "./utils/clipboard";
22
- import {
23
- STATE_ORDER,
24
- PRIORITY_ORDER,
25
- ATTENTION_LABELS,
26
- SPEC_LABELS,
27
- shortLabelName,
28
- currentStateLabel,
29
- currentPriorityLabel,
30
- nextStateLabel,
31
- parseLabelList,
32
- writeGateNotice,
33
- stripAgentEnvelopeFooter,
34
- parseMarkdownLite,
35
- issueDetailContract,
36
- setLabelContract,
37
- addCommentContract,
38
- createIssueContract,
39
- type ForgeIssue,
40
- type IssueComment,
41
- type AgentEnvelope,
42
- type ForgeAccessState,
43
- type ForgeLabel,
44
- type MarkdownLiteSpan,
45
- } from "../../../shared/vendor/paseo-plugin-helper/tickets";
46
-
47
- function formatTimestamp(value: string | undefined | null): string {
48
- if (!value) return "unknown";
49
- const parsed = new Date(value);
50
- if (Number.isNaN(parsed.getTime())) return value;
51
- return parsed.toLocaleString();
52
- }
53
-
54
- function editorLabelChips(
55
- names: readonly string[],
56
- known: Map<string, ForgeLabel>,
57
- ): ForgeLabel[] {
58
- return names.map((name) => known.get(name) ?? { name });
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // MarkdownLite
63
- // ---------------------------------------------------------------------------
64
-
65
- function renderInlineSpans(
66
- spans: MarkdownLiteSpan[],
67
- colors: { foreground: string; accent: string },
68
- keyPrefix: string,
69
- query: string,
70
- ) {
71
- return spans.map((span, index) => {
72
- const key = `${keyPrefix}-${index}`;
73
- if (span.kind === "bold") {
74
- return (
75
- <Text key={key} style={styles.bold}>
76
- <HighlightedText text={span.text} query={query} />
77
- </Text>
78
- );
79
- }
80
- if (span.kind === "italic") {
81
- return (
82
- <Text key={key} style={styles.italic}>
83
- <HighlightedText text={span.text} query={query} />
84
- </Text>
85
- );
86
- }
87
- if (span.kind === "code") {
88
- return (
89
- <Text key={key} style={[styles.inlineCode, { backgroundColor: colors.accent + "18" }]}>
90
- <HighlightedText text={span.text} query={query} />
91
- </Text>
92
- );
93
- }
94
- if (span.kind === "link") {
95
- return (
96
- <Text
97
- key={key}
98
- style={[styles.link, { color: colors.accent }]}
99
- onPress={() => {
100
- Linking.openURL(span.url).catch(() => {
101
- copyToClipboard(span.url).catch(() => {});
102
- });
103
- }}
104
- >
105
- <HighlightedText text={span.text} query={query} />
106
- </Text>
107
- );
108
- }
109
- return <HighlightedText key={key} text={span.text} query={query} />;
110
- });
111
- }
112
-
113
- export function MarkdownLite({
114
- body,
115
- query = "",
116
- }: {
117
- body: string;
118
- query?: string;
119
- }) {
120
- const { colors, typography } = usePluginTheme();
121
- const blocks = useMemo(() => parseMarkdownLite(body), [body]);
122
-
123
- return (
124
- <Stack gap="xs" style={styles.markdownContainer}>
125
- {blocks.map((block, index) => {
126
- if (block.kind === "code") {
127
- return <CodeBlock key={index} code={block.text} language={block.language} />;
128
- }
129
- if (block.kind === "heading") {
130
- const fontSize = block.level === 1 ? 18 : block.level === 2 ? 16 : 14;
131
- return (
132
- <Text
133
- key={index}
134
- selectable
135
- style={[
136
- typography.heading,
137
- { fontSize, fontWeight: "700", color: colors.foreground, marginTop: 4 },
138
- ]}
139
- >
140
- {renderInlineSpans(block.spans, colors, `h${index}`, query)}
141
- </Text>
142
- );
143
- }
144
- if (block.kind === "list") {
145
- return (
146
- <Stack key={index} gap="xxs" style={styles.listContainer}>
147
- {block.items.map((item, itemIndex) => (
148
- <Row key={itemIndex} align="flex-start" gap="xs">
149
- <Text selectable style={[typography.body, { color: colors.foregroundMuted }]}>
150
- {block.ordered ? `${itemIndex + 1}.` : "•"}
151
- </Text>
152
- <Text selectable style={[typography.body, { color: colors.foreground, flex: 1 }]}>
153
- {renderInlineSpans(item, colors, `li${index}-${itemIndex}`, query)}
154
- </Text>
155
- </Row>
156
- ))}
157
- </Stack>
158
- );
159
- }
160
- return (
161
- <Text
162
- key={index}
163
- selectable
164
- style={[typography.body, { color: colors.foreground, lineHeight: 20 }]}
165
- >
166
- {renderInlineSpans(block.spans, colors, `p${index}`, query)}
167
- </Text>
168
- );
169
- })}
170
- </Stack>
171
- );
172
- }
173
-
174
- // ---------------------------------------------------------------------------
175
- // AgentEnvelopeCard
176
- // ---------------------------------------------------------------------------
177
-
178
- export function AgentEnvelopeCard({ envelope }: { envelope: AgentEnvelope }) {
179
- const openSession = () => {
180
- const link = envelope.paseoLinks[0];
181
- if (!link) return;
182
- Linking.openURL(link).catch(() => {
183
- copyToClipboard(link).catch(() => {});
184
- });
185
- };
186
-
187
- return (
188
- <Card variant="tinted" style={styles.envelopeCard}>
189
- <Card.Header
190
- title={envelope.sessionTitle}
191
- subtitle={envelope.postedAt ?? undefined}
192
- badge={<Badge variant="neutral" label={envelope.agentShortId} />}
193
- icon="Bot"
194
- />
195
- <KeyValueGroup columns={2}>
196
- <KeyValue label="Model" value={envelope.model ?? "-"} mono />
197
- <KeyValue
198
- label="Branch"
199
- value={envelope.branch ? `${envelope.repo ?? ""}:${envelope.branch}` : "-"}
200
- mono
201
- copyable={Boolean(envelope.branch)}
202
- />
203
- </KeyValueGroup>
204
- {envelope.commitShas.length > 0 ? (
205
- <Stack gap="xxs" style={styles.shaList}>
206
- {envelope.commitShas.map((sha) => (
207
- <CommandBox key={sha} command={sha} copyLabel={`Copy commit ${sha}`} />
208
- ))}
209
- </Stack>
210
- ) : null}
211
- {envelope.paseoLinks.length > 0 ? (
212
- <Button
213
- size="sm"
214
- variant="ghost"
215
- icon="ExternalLink"
216
- label="Open agent session"
217
- onPress={openSession}
218
- />
219
- ) : null}
220
- </Card>
221
- );
222
- }
223
-
224
- // ---------------------------------------------------------------------------
225
- // CommentCard
226
- // ---------------------------------------------------------------------------
227
-
228
- export function CommentCard({
229
- comment,
230
- issueUrl,
231
- query,
232
- }: {
233
- comment: IssueComment;
234
- issueUrl?: string;
235
- query?: string;
236
- }) {
237
- const body = stripAgentEnvelopeFooter(comment.body) || comment.body;
238
- const target = comment.url || issueUrl || "";
239
- const open = () => {
240
- if (target) Linking.openURL(target).catch(() => {});
241
- };
242
-
243
- return (
244
- <Card variant="flat" style={styles.commentCard}>
245
- <Card.Header
246
- title={comment.author}
247
- subtitle={formatTimestamp(comment.createdAt)}
248
- icon="MessageSquare"
249
- />
250
- <Stack gap="xs" style={styles.commentContent}>
251
- <MarkdownLite body={body} query={query} />
252
- {target ? (
253
- <Row justify="flex-end" align="center" gap="xs">
254
- <Button
255
- size="sm"
256
- variant="ghost"
257
- icon="ExternalLink"
258
- label="Open comment"
259
- accessibilityLabel={`Open comment by ${comment.author}`}
260
- onPress={open}
261
- />
262
- <CopyButton text={target} label="Copy" accessibilityLabel="Copy comment link" />
263
- </Row>
264
- ) : null}
265
- </Stack>
266
- {comment.envelope ? <AgentEnvelopeCard envelope={comment.envelope} /> : null}
267
- </Card>
268
- );
269
- }
270
-
271
- // ---------------------------------------------------------------------------
272
- // ScopedLabelGroup
273
- // ---------------------------------------------------------------------------
274
-
275
- export function ScopedLabelGroup({
276
- title,
277
- labels,
278
- active,
279
- pending = false,
280
- onSelect,
281
- }: {
282
- title: string;
283
- labels: readonly ForgeLabel[];
284
- active: string | null;
285
- pending?: boolean;
286
- onSelect: (label: string) => void;
287
- }) {
288
- const { colors, typography } = usePluginTheme();
289
-
290
- return (
291
- <Stack gap="xxs" style={styles.labelGroup}>
292
- <Text style={[typography.caption, { color: colors.foregroundMuted, fontWeight: "600" }]}>
293
- {title}
294
- </Text>
295
- <Row wrap gap="xxs">
296
- {labels.map((label) => {
297
- const isSelected = active === label.name;
298
- return (
299
- <Button
300
- key={label.name}
301
- onPress={() => onSelect(label.name)}
302
- disabled={pending}
303
- accessibilityLabel={shortLabelName(label.name)}
304
- size="sm"
305
- variant={isSelected ? "primary" : "ghost"}
306
- label={shortLabelName(label.name)}
307
- />
308
- );
309
- })}
310
- </Row>
311
- </Stack>
312
- );
313
- }
314
-
315
- // ---------------------------------------------------------------------------
316
- // NewIssueComposer
317
- // ---------------------------------------------------------------------------
318
-
319
- export function NewIssueComposer({
320
- directory,
321
- remoteUrl,
322
- access,
323
- onCreated,
324
- onOpenSettings,
325
- }: {
326
- directory?: string;
327
- remoteUrl?: string;
328
- access?: ForgeAccessState;
329
- onCreated?: (number?: number) => void;
330
- onOpenSettings?: () => void;
331
- }) {
332
- const { colors, typography } = usePluginTheme();
333
- const { useToast } = getClientHost();
334
- const toast = useToast();
335
- const [expanded, setExpanded] = useState(false);
336
- const [title, setTitle] = useState("");
337
- const [body, setBody] = useState("");
338
- const [labelsText, setLabelsText] = useState("");
339
-
340
- const create = useRpcMutation(createIssueContract, {
341
- onSuccess: (result) => {
342
- if (result.error) {
343
- toast?.error?.(result.error);
344
- return;
345
- }
346
- setTitle("");
347
- setBody("");
348
- setLabelsText("");
349
- setExpanded(false);
350
- onCreated?.(result.number ?? undefined);
351
- toast?.show?.(result.number ? `Created issue #${result.number}` : "Issue created", {
352
- variant: "success",
353
- });
354
- },
355
- onError: (error) => {
356
- toast?.error?.(error instanceof Error ? error.message : "Could not create issue");
357
- },
358
- });
359
-
360
- if (access && access.auth === "anonymous") return null;
361
- if (access && !access.canEdit) {
362
- return (
363
- <Card variant="flat" style={styles.readOnlyCard}>
364
- <Text style={[typography.caption, { color: colors.foregroundMuted }]}>
365
- {writeGateNotice(access, "creating issues")}
366
- </Text>
367
- {onOpenSettings && (
368
- <Button
369
- size="sm"
370
- variant="secondary"
371
- icon="Settings"
372
- label="Add a token"
373
- onPress={onOpenSettings}
374
- />
375
- )}
376
- </Card>
377
- );
378
- }
379
-
380
- const pending = create.isPending;
381
- const canSubmit = title.trim().length > 0 && !pending;
382
-
383
- const submit = () => {
384
- const nextTitle = title.trim();
385
- if (!nextTitle) return;
386
- create.mutate({
387
- directory: directory ?? undefined,
388
- remoteUrl: remoteUrl || undefined,
389
- title: nextTitle,
390
- body: body.trim(),
391
- labels: parseLabelList(labelsText),
392
- });
393
- };
394
-
395
- return (
396
- <Collapsible
397
- title="New ticket"
398
- subtitle="Open a ticket on the active forge"
399
- icon="Plus"
400
- isExpanded={expanded}
401
- onToggle={setExpanded}
402
- >
403
- <Stack gap="xs" style={styles.composerForm}>
404
- <FormRow label="Title">
405
- <TextInput
406
- value={title}
407
- onChangeText={setTitle}
408
- placeholder="Short, specific title"
409
- autoCapitalize="sentences"
410
- />
411
- </FormRow>
412
- <FormRow label="Description" description="Markdown supported.">
413
- <TextInput
414
- value={body}
415
- onChangeText={setBody}
416
- placeholder="What needs to happen?"
417
- multiline
418
- numberOfLines={4}
419
- />
420
- </FormRow>
421
- <Collapsible title="Labels" subtitle="Optional — comma-separated names" icon="Tags">
422
- <FormRow label="Label names">
423
- <TextInput
424
- value={labelsText}
425
- onChangeText={setLabelsText}
426
- placeholder="state/1-wip, priority/1-high"
427
- />
428
- </FormRow>
429
- </Collapsible>
430
- <ActionBar>
431
- <Button
432
- label="Cancel"
433
- variant="ghost"
434
- disabled={pending}
435
- onPress={() => setExpanded(false)}
436
- />
437
- <Button
438
- label={pending ? "Creating…" : "Create ticket"}
439
- variant="primary"
440
- icon="Plus"
441
- disabled={!canSubmit}
442
- loading={pending}
443
- onPress={submit}
444
- />
445
- </ActionBar>
446
- </Stack>
447
- </Collapsible>
448
- );
449
- }
450
-
451
- // ---------------------------------------------------------------------------
452
- // TicketLifecycleView
453
- // ---------------------------------------------------------------------------
454
-
455
- export interface TicketLifecycleViewProps {
456
- issueNumber: number;
457
- workspaceId?: string;
458
- directory?: string;
459
- remoteUrl?: string;
460
- repo?: string | null;
461
- query?: string;
462
- boardLabels?: Map<string, ForgeLabel>;
463
- onRefresh?: () => void;
464
- onOpenSettings?: () => void;
465
- children?: React.ReactNode;
466
- }
467
-
468
- export function TicketLifecycleView({
469
- issueNumber,
470
- workspaceId: _workspaceId,
471
- directory,
472
- remoteUrl,
473
- repo: _repo,
474
- query = "",
475
- boardLabels,
476
- onRefresh,
477
- onOpenSettings: _onOpenSettings,
478
- children,
479
- }: TicketLifecycleViewProps) {
480
- const { colors, typography } = usePluginTheme();
481
- const { useToast } = getClientHost();
482
- const toast = useToast();
483
-
484
- const baseInput = {
485
- issueNumber,
486
- directory: directory ?? undefined,
487
- remoteUrl: remoteUrl || undefined,
488
- };
489
-
490
- const detail = useRpcQuery(
491
- issueDetailContract,
492
- baseInput,
493
- { refetchInterval: 30000 },
494
- );
495
-
496
- const [commentDraft, setCommentDraft] = useState("");
497
-
498
- const setLabel = useRpcMutation(setLabelContract, {
499
- onSuccess: (result) => {
500
- if (result.error) {
501
- toast?.error?.(result.error);
502
- return;
503
- }
504
- detail.refetch();
505
- onRefresh?.();
506
- },
507
- onError: (error) => {
508
- toast?.error?.(error instanceof Error ? error.message : "Could not update labels");
509
- },
510
- });
511
-
512
- const addComment = useRpcMutation(addCommentContract, {
513
- onSuccess: (result) => {
514
- if (result.error) {
515
- toast?.error?.(result.error);
516
- return;
517
- }
518
- setCommentDraft("");
519
- detail.refetch();
520
- onRefresh?.();
521
- toast?.show?.("Comment posted", { variant: "success" });
522
- },
523
- onError: (error) => {
524
- toast?.error?.(error instanceof Error ? error.message : "Could not post comment");
525
- },
526
- });
527
-
528
- const issue = detail.data && !detail.data.error ? detail.data.issue : null;
529
- const labels = issue?.labels ?? [];
530
- const state = currentStateLabel(labels);
531
- const priority = issue ? currentPriorityLabel(labels) : null;
532
- const next = issue ? nextStateLabel(labels) : null;
533
- const labelPending = setLabel.isPending;
534
- const commentPending = addComment.isPending;
535
-
536
- const candidates = useMemo(() => {
537
- const known = new Map(boardLabels ?? new Map());
538
- for (const label of issue?.labelDetails ?? []) known.set(label.name, label);
539
- return {
540
- state: editorLabelChips(STATE_ORDER, known),
541
- priority: editorLabelChips(PRIORITY_ORDER, known),
542
- attention: editorLabelChips(ATTENTION_LABELS, known),
543
- spec: editorLabelChips(SPEC_LABELS, known),
544
- };
545
- }, [boardLabels, issue?.labelDetails]);
546
-
547
- const handleSelectLabel = (labelName: string) => {
548
- setLabel.mutate({
549
- issueNumber,
550
- directory: directory ?? undefined,
551
- remoteUrl: remoteUrl || undefined,
552
- label: labelName,
553
- });
554
- };
555
-
556
- const handlePostComment = () => {
557
- const text = commentDraft.trim();
558
- if (!text) return;
559
- addComment.mutate({
560
- issueNumber,
561
- directory: directory ?? undefined,
562
- remoteUrl: remoteUrl || undefined,
563
- body: text,
564
- });
565
- };
566
-
567
- return (
568
- <Stack gap="sm" style={styles.lifecycleContainer}>
569
- {detail.isLoading && !detail.data ? (
570
- <Text style={[typography.caption, { color: colors.foregroundMuted }]}>
571
- Loading ticket #{issueNumber}…
572
- </Text>
573
- ) : null}
574
-
575
- {detail.data?.error || (!detail.isLoading && !issue) ? (
576
- <EmptyState
577
- icon="AlertCircle"
578
- title={`Ticket #${issueNumber} unavailable`}
579
- description={detail.data?.error ?? "Could not reach forge host."}
580
- actionLabel="Retry"
581
- onAction={() => detail.refetch()}
582
- />
583
- ) : null}
584
-
585
- {/* Label Management: Next state promotion & Scoped label groups */}
586
- <Stack gap="xs" style={styles.sectionBlock}>
587
- {next && (
588
- <Row align="center" gap="xs">
589
- <Button
590
- size="sm"
591
- variant="secondary"
592
- icon="ArrowRight"
593
- label={`Move to ${shortLabelName(next).toLowerCase()}`}
594
- loading={labelPending}
595
- disabled={labelPending}
596
- onPress={() => handleSelectLabel(next)}
597
- />
598
- </Row>
599
- )}
600
-
601
- <ScopedLabelGroup
602
- title="Workflow State"
603
- labels={candidates.state}
604
- active={state}
605
- pending={labelPending}
606
- onSelect={handleSelectLabel}
607
- />
608
-
609
- <ScopedLabelGroup
610
- title="Priority"
611
- labels={candidates.priority}
612
- active={priority}
613
- pending={labelPending}
614
- onSelect={handleSelectLabel}
615
- />
616
-
617
- <ScopedLabelGroup
618
- title="Attention"
619
- labels={candidates.attention}
620
- active={labels.find((l: string) => (ATTENTION_LABELS as readonly string[]).includes(l)) ?? null}
621
- pending={labelPending}
622
- onSelect={handleSelectLabel}
623
- />
624
-
625
- <ScopedLabelGroup
626
- title="Spec"
627
- labels={candidates.spec}
628
- active={labels.find((l: string) => (SPEC_LABELS as readonly string[]).includes(l)) ?? null}
629
- pending={labelPending}
630
- onSelect={handleSelectLabel}
631
- />
632
- </Stack>
633
-
634
- {/* Embedded children (e.g. Worktree branch KeyValue, Close, Dispatch buttons) */}
635
- {children}
636
-
637
- {/* Ticket Description */}
638
- {issue?.body ? (
639
- <Card variant="flat" style={styles.bodyCard}>
640
- <Card.Header title="Description" icon="FileText" />
641
- <View style={styles.bodyPadding}>
642
- <MarkdownLite body={issue.body} query={query} />
643
- </View>
644
- </Card>
645
- ) : null}
646
-
647
- {/* Comments List & Adding Comments */}
648
- <Card variant="flat" style={styles.commentsCard}>
649
- <Card.Header
650
- title={`Comments${issue?.comments ? ` (${issue.comments.length})` : ""}`}
651
- icon="MessageSquare"
652
- />
653
- <Stack gap="xs" style={styles.commentsList}>
654
- {issue?.comments && issue.comments.length > 0 ? (
655
- issue.comments.map((comment: IssueComment) => (
656
- <CommentCard
657
- key={comment.id}
658
- comment={comment}
659
- issueUrl={issue.webUrl}
660
- query={query}
661
- />
662
- ))
663
- ) : (
664
- <Text style={[typography.caption, { color: colors.foregroundMuted }]}>
665
- No comments yet.
666
- </Text>
667
- )}
668
-
669
- {/* Add Comment input */}
670
- <Stack gap="xxs" style={styles.newCommentBox}>
671
- <TextInput
672
- value={commentDraft}
673
- onChangeText={setCommentDraft}
674
- placeholder="Write a comment…"
675
- multiline
676
- numberOfLines={3}
677
- />
678
- <Row justify="flex-end">
679
- <Button
680
- size="sm"
681
- variant="primary"
682
- icon="Send"
683
- label={commentPending ? "Posting…" : "Post comment"}
684
- disabled={!commentDraft.trim() || commentPending}
685
- loading={commentPending}
686
- onPress={handlePostComment}
687
- />
688
- </Row>
689
- </Stack>
690
- </Stack>
691
- </Card>
692
- </Stack>
693
- );
694
- }
695
-
696
- const styles = StyleSheet.create({
697
- markdownContainer: {
698
- width: "100%",
699
- },
700
- bold: {
701
- fontWeight: "700",
702
- },
703
- italic: {
704
- fontStyle: "italic",
705
- },
706
- inlineCode: {
707
- fontFamily: "monospace",
708
- fontSize: 12,
709
- paddingHorizontal: 4,
710
- borderRadius: 3,
711
- },
712
- link: {
713
- textDecorationLine: "underline",
714
- },
715
- listContainer: {
716
- paddingLeft: 4,
717
- },
718
- envelopeCard: {
719
- marginTop: 6,
720
- },
721
- shaList: {
722
- marginTop: 4,
723
- },
724
- commentCard: {
725
- marginVertical: 4,
726
- },
727
- commentContent: {
728
- padding: 8,
729
- },
730
- labelGroup: {
731
- marginVertical: 2,
732
- },
733
- readOnlyCard: {
734
- padding: 8,
735
- flexDirection: "row",
736
- alignItems: "center",
737
- justifyContent: "space-between",
738
- },
739
- composerForm: {
740
- padding: 8,
741
- },
742
- lifecycleContainer: {
743
- width: "100%",
744
- },
745
- sectionBlock: {
746
- paddingVertical: 4,
747
- },
748
- bodyCard: {
749
- marginVertical: 4,
750
- },
751
- bodyPadding: {
752
- padding: 8,
753
- },
754
- commentsCard: {
755
- marginTop: 8,
756
- },
757
- commentsList: {
758
- padding: 8,
759
- },
760
- newCommentBox: {
761
- marginTop: 8,
762
- paddingTop: 8,
763
- borderTopWidth: 1,
764
- borderTopColor: "rgba(128, 128, 128, 0.2)",
765
- },
766
- });