@frockbot/plugin-shell 0.0.0 → 0.1.1

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 (73) hide show
  1. package/frockbot.json +68 -0
  2. package/package.json +87 -6
  3. package/src/agent.test.ts +372 -0
  4. package/src/agent.ts +335 -0
  5. package/src/approvals.test.ts +224 -0
  6. package/src/approvals.ts +530 -0
  7. package/src/backend-assignment.test.ts +161 -0
  8. package/src/backend-assignment.ts +274 -0
  9. package/src/backend-authoring.test.ts +518 -0
  10. package/src/backend-authoring.ts +531 -0
  11. package/src/backend-bot-identity.test.ts +215 -0
  12. package/src/backend-completion.test.ts +289 -0
  13. package/src/backend-completion.ts +95 -0
  14. package/src/backend-composition.ts +242 -0
  15. package/src/backend-computer.ts +76 -0
  16. package/src/backend-configuration.test.ts +1757 -0
  17. package/src/backend-contracts.test.ts +189 -0
  18. package/src/backend-contracts.ts +44 -0
  19. package/src/backend-debug.test.ts +202 -0
  20. package/src/backend-execution.ts +55 -0
  21. package/src/backend-flock.ts +96 -0
  22. package/src/backend-image.test.ts +115 -0
  23. package/src/backend-image.ts +180 -0
  24. package/src/backend-isolate.test.ts +238 -0
  25. package/src/backend-isolate.ts +409 -0
  26. package/src/backend-machine.ts +144 -0
  27. package/src/backend-memory.ts +89 -0
  28. package/src/backend-recovery-integration.test.ts +1575 -0
  29. package/src/backend-recovery.ts +106 -0
  30. package/src/backend-routines.ts +375 -0
  31. package/src/backend-runner.ts +251 -0
  32. package/src/backend-skills.test.ts +126 -0
  33. package/src/backend-skills.ts +198 -0
  34. package/src/backend-stop.test.ts +356 -0
  35. package/src/backend-subagents.ts +459 -0
  36. package/src/backend.ts +6035 -0
  37. package/src/client/FrockBotApp.vue +1026 -0
  38. package/src/client/SendPayloadView.vue +337 -0
  39. package/src/client/composer-draft.test.ts +31 -0
  40. package/src/client/composer-draft.ts +35 -0
  41. package/src/client/cordis-client-shim.d.ts +15 -0
  42. package/src/client/index.test.ts +2548 -0
  43. package/src/client/index.ts +2346 -0
  44. package/src/client/model-presentation.test.ts +35 -0
  45. package/src/client/model-presentation.ts +19 -0
  46. package/src/client/notify.test.ts +89 -0
  47. package/src/client/notify.ts +101 -0
  48. package/src/client/skill-invocation.test.ts +143 -0
  49. package/src/client/skill-invocation.ts +175 -0
  50. package/src/client/styles.css +1043 -0
  51. package/src/composition-views.ts +118 -0
  52. package/src/debug-protocol.test.ts +80 -0
  53. package/src/debug-protocol.ts +165 -0
  54. package/src/env.d.ts +10 -0
  55. package/src/history.test.ts +163 -0
  56. package/src/history.ts +108 -0
  57. package/src/host.ts +20 -0
  58. package/src/index.ts +2 -0
  59. package/src/manifest.ts +3 -0
  60. package/src/run-cursor.ts +28 -0
  61. package/src/run-protocol.test.ts +1281 -0
  62. package/src/run-protocol.ts +1417 -0
  63. package/src/settings-links.test.ts +106 -0
  64. package/src/settings-links.ts +289 -0
  65. package/src/shared.ts +338 -0
  66. package/src/skill-protocol.ts +117 -0
  67. package/src/terminal-records.test.ts +217 -0
  68. package/src/terminal-records.ts +150 -0
  69. package/src/unread.test.ts +362 -0
  70. package/src/unread.ts +675 -0
  71. package/tsconfig.json +18 -0
  72. package/vite.config.ts +32 -0
  73. package/README.md +0 -3
@@ -0,0 +1,337 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * One user-facing send, drawn in the thread on the Bot's side.
4
+ *
5
+ * Read-only by design, with one exception. A widget shows the question and its
6
+ * options but does not answer it: answering queues a normal chat Turn, which is
7
+ * a later slice, and a control that looks live but is not would be worse than
8
+ * none. An approval card *is* live, because its answer is not a Turn — it is a
9
+ * durable decision recorded against a record the Bot Durable Object already
10
+ * holds, and the buttons submit exactly that command.
11
+ *
12
+ * The card renders the decision the backend reports, never the click: a card
13
+ * answered on another device, or expired by the alarm, shows what was actually
14
+ * recorded.
15
+ *
16
+ * Anything this client cannot draw — a payload shape newer than this bundle,
17
+ * or one the decoder refused — becomes a plain line saying so. A Turn's
18
+ * history has to render on a client older than the Bot that produced it.
19
+ */
20
+ import { UiButton, UiMarkdown } from "@frockbot/client-ui";
21
+ import { computed, inject, ref } from "vue";
22
+ import { settingsLinkV1 } from "../settings-links.js";
23
+ import { frockBotWebDataKey, type WebSendPayload } from "../shared.js";
24
+
25
+ const props = defineProps<{ send: WebSendPayload }>();
26
+
27
+ const web = inject(frockBotWebDataKey);
28
+ const deciding = ref(false);
29
+
30
+ const payload = computed(() =>
31
+ props.send.kind === "payload" ? props.send.payload : undefined,
32
+ );
33
+ const text = computed(() =>
34
+ payload.value?.type === "text" ? payload.value.text : undefined,
35
+ );
36
+ const widget = computed(() =>
37
+ payload.value?.type === "widget" ? payload.value.widget : undefined,
38
+ );
39
+ const attachment = computed(() =>
40
+ payload.value?.type === "attachment" ? payload.value : undefined,
41
+ );
42
+ /**
43
+ * The connect card. It carries no URL and never will: the Bot recorded a
44
+ * pending decision, and only the User — in Settings, where the host authors
45
+ * the link at the moment they press it — can complete one. So this draws the
46
+ * request and points at the place the decision is made, and is deliberately
47
+ * not a button that authorizes anything.
48
+ */
49
+ const connectCard = computed(() =>
50
+ payload.value?.type === "connect-card" ? payload.value : undefined,
51
+ );
52
+ const connectionsLink = settingsLinkV1({ anchor: "user-packages" });
53
+ const approval = computed(() =>
54
+ payload.value?.type === "approval" ? payload.value : undefined,
55
+ );
56
+
57
+ /** What the backend says about this card, when it has been read. */
58
+ const record = computed(() =>
59
+ approval.value
60
+ ? web?.value.approvals.find(
61
+ (candidate) => candidate.approvalId === approval.value?.approvalId,
62
+ )
63
+ : undefined,
64
+ );
65
+
66
+ const decision = computed(() => record.value?.decision ?? "pending");
67
+
68
+ const decided = computed(() =>
69
+ decision.value === "approved"
70
+ ? "You approved this."
71
+ : decision.value === "denied"
72
+ ? "You denied this."
73
+ : decision.value === "expired"
74
+ ? "This expired before anyone answered."
75
+ : undefined,
76
+ );
77
+
78
+ const expiry = computed(() => {
79
+ const at = record.value?.expiresAt;
80
+ if (!at || decision.value !== "pending") return undefined;
81
+ const parsed = new Date(at);
82
+ return Number.isNaN(parsed.getTime())
83
+ ? undefined
84
+ : `Expires ${parsed.toLocaleString()}`;
85
+ });
86
+
87
+ async function decide(next: "approved" | "denied"): Promise<void> {
88
+ if (!web || !approval.value || deciding.value) return;
89
+ deciding.value = true;
90
+ try {
91
+ await web.value.decideApproval(approval.value.approvalId, next);
92
+ } finally {
93
+ deciding.value = false;
94
+ }
95
+ }
96
+
97
+ const unsupported = computed(
98
+ () => !text.value && !widget.value && !attachment.value && !approval.value,
99
+ );
100
+ </script>
101
+
102
+ <template>
103
+ <div v-if="text" class="send-text"><UiMarkdown :text="text" /></div>
104
+
105
+ <section v-else-if="widget" class="send-widget" aria-label="Question">
106
+ <p class="send-widget-prompt">{{ widget.prompt }}</p>
107
+ <p v-if="widget.helpText" class="send-widget-help">
108
+ {{ widget.helpText }}
109
+ </p>
110
+ <ul class="send-widget-options">
111
+ <li v-for="option in widget.options" :key="option">{{ option }}</li>
112
+ </ul>
113
+ <p v-if="widget.allowCustom" class="send-widget-help">
114
+ Any other answer is accepted too.
115
+ </p>
116
+ </section>
117
+
118
+ <section
119
+ v-else-if="connectCard"
120
+ class="send-connect-card"
121
+ aria-label="Connection request"
122
+ >
123
+ <p class="send-connect-title">{{ connectCard.title }}</p>
124
+ <p v-if="connectCard.body" class="send-connect-body">
125
+ {{ connectCard.body }}
126
+ </p>
127
+ <p class="send-connect-help">
128
+ Open Settings → Plugins to authorize it. Only you can complete this.
129
+ <a :href="connectionsLink">{{ connectionsLink }}</a>
130
+ </p>
131
+ </section>
132
+
133
+ <section
134
+ v-else-if="approval"
135
+ class="send-approval"
136
+ :class="`send-approval--${decision}`"
137
+ aria-label="Approval request"
138
+ >
139
+ <header class="send-approval-head">
140
+ <span class="send-approval-risk" :class="`risk-${approval.risk}`">{{
141
+ approval.risk
142
+ }}</span>
143
+ <span class="send-approval-label">Needs your approval</span>
144
+ </header>
145
+ <p class="send-approval-action">{{ approval.action }}</p>
146
+ <p v-if="approval.rationale" class="send-approval-rationale">
147
+ {{ approval.rationale }}
148
+ </p>
149
+ <p v-if="decided" class="send-approval-decided">{{ decided }}</p>
150
+ <div v-else class="send-approval-actions">
151
+ <UiButton :disabled="deciding || !web" @click="decide('approved')"
152
+ >Approve</UiButton
153
+ >
154
+ <UiButton
155
+ variant="ghost"
156
+ :disabled="deciding || !web"
157
+ @click="decide('denied')"
158
+ >Deny</UiButton
159
+ >
160
+ </div>
161
+ <p v-if="expiry" class="send-approval-expiry">{{ expiry }}</p>
162
+ </section>
163
+
164
+ <p v-else-if="attachment" class="send-attachment">
165
+ <a :href="attachment.url" target="_blank" rel="noopener noreferrer">{{
166
+ attachment.name ?? attachment.url
167
+ }}</a>
168
+ </p>
169
+
170
+ <p v-else-if="unsupported" class="send-unsupported">
171
+ This client cannot display that message.
172
+ </p>
173
+ </template>
174
+
175
+ <style scoped>
176
+ /* An assistant-side bubble, shaped like the one the derived text uses. */
177
+ .send-text {
178
+ width: max-content;
179
+ max-width: 100%;
180
+ border: 1px solid var(--frock-border);
181
+ border-radius: 18px 18px 18px 6px;
182
+ background: var(--frock-surface-subtle);
183
+ padding: 10px 14px;
184
+ color: var(--frock-text);
185
+ font-size: var(--frock-text-md);
186
+ line-height: var(--frock-leading-normal);
187
+ overflow-wrap: anywhere;
188
+ }
189
+
190
+ .send-widget {
191
+ display: flex;
192
+ flex-direction: column;
193
+ gap: 0.5rem;
194
+ border: 1px solid var(--frock-border);
195
+ border-radius: var(--frock-radius-card);
196
+ background: var(--frock-surface-raised);
197
+ padding: 0.75rem 1rem;
198
+ }
199
+
200
+ .send-widget-prompt {
201
+ margin: 0;
202
+ color: var(--frock-text);
203
+ font-weight: 600;
204
+ font-size: var(--frock-text-base);
205
+ line-height: var(--frock-leading-snug);
206
+ }
207
+
208
+ .send-widget-help {
209
+ margin: 0;
210
+ color: var(--frock-text-muted);
211
+ font-size: var(--frock-text-sm);
212
+ line-height: var(--frock-leading-normal);
213
+ }
214
+
215
+ .send-widget-options {
216
+ display: flex;
217
+ flex-wrap: wrap;
218
+ gap: 0.375rem;
219
+ margin: 0;
220
+ padding: 0;
221
+ list-style: none;
222
+ }
223
+
224
+ .send-connect-card {
225
+ display: flex;
226
+ flex-direction: column;
227
+ gap: 0.5rem;
228
+ border: 1px solid var(--frock-border);
229
+ border-radius: var(--frock-radius-card);
230
+ background: var(--frock-surface-raised);
231
+ padding: 0.75rem 1rem;
232
+ }
233
+
234
+ .send-connect-title {
235
+ margin: 0;
236
+ color: var(--frock-text);
237
+ font-weight: 600;
238
+ font-size: var(--frock-text-base);
239
+ line-height: var(--frock-leading-snug);
240
+ }
241
+
242
+ .send-connect-body {
243
+ margin: 0;
244
+ color: var(--frock-text);
245
+ font-size: var(--frock-text-sm);
246
+ line-height: var(--frock-leading-normal);
247
+ }
248
+
249
+ .send-connect-help {
250
+ margin: 0;
251
+ color: var(--frock-text-muted);
252
+ font-size: var(--frock-text-sm);
253
+ line-height: var(--frock-leading-normal);
254
+ overflow-wrap: anywhere;
255
+ }
256
+
257
+ .send-widget-options li {
258
+ border: 1px solid var(--frock-border);
259
+ border-radius: var(--frock-radius-control);
260
+ background: var(--frock-surface);
261
+ padding: 0.25rem 0.625rem;
262
+ color: var(--frock-text);
263
+ font-size: var(--frock-text-sm);
264
+ }
265
+
266
+ .send-approval {
267
+ display: flex;
268
+ flex-direction: column;
269
+ gap: 0.5rem;
270
+ border: 1px solid var(--frock-border);
271
+ border-radius: var(--frock-radius-card);
272
+ background: var(--frock-surface-raised);
273
+ padding: 0.75rem 1rem;
274
+ }
275
+
276
+ .send-approval-head {
277
+ display: flex;
278
+ gap: 0.5rem;
279
+ align-items: center;
280
+ }
281
+
282
+ .send-approval-risk {
283
+ border: 1px solid var(--frock-border);
284
+ border-radius: var(--frock-radius-control);
285
+ padding: 0.125rem 0.5rem;
286
+ color: var(--frock-text-muted);
287
+ font-size: var(--frock-text-xs);
288
+ text-transform: uppercase;
289
+ }
290
+
291
+ .send-approval-risk.risk-high {
292
+ border-color: var(--frock-danger-border);
293
+ color: var(--frock-danger-text);
294
+ }
295
+
296
+ .send-approval-label {
297
+ color: var(--frock-text-muted);
298
+ font-size: var(--frock-text-sm);
299
+ }
300
+
301
+ .send-approval-action {
302
+ margin: 0;
303
+ color: var(--frock-text);
304
+ font-weight: 600;
305
+ font-size: var(--frock-text-base);
306
+ line-height: var(--frock-leading-snug);
307
+ }
308
+
309
+ .send-approval-rationale,
310
+ .send-approval-decided,
311
+ .send-approval-expiry {
312
+ margin: 0;
313
+ color: var(--frock-text-muted);
314
+ font-size: var(--frock-text-sm);
315
+ line-height: var(--frock-leading-normal);
316
+ }
317
+
318
+ .send-approval-actions {
319
+ display: flex;
320
+ gap: 0.5rem;
321
+ }
322
+
323
+ .send-attachment,
324
+ .send-unsupported {
325
+ margin: 0;
326
+ font-size: var(--frock-text-sm);
327
+ }
328
+
329
+ .send-attachment a {
330
+ color: var(--frock-accent-text);
331
+ }
332
+
333
+ .send-unsupported {
334
+ color: var(--frock-text-muted);
335
+ font-style: italic;
336
+ }
337
+ </style>
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { ComposerDraftStore } from "./composer-draft.js";
3
+
4
+ describe("composer draft restoration", () => {
5
+ test("retains a rejected submission in its originating Bot draft", () => {
6
+ const drafts = new ComposerDraftStore();
7
+ const submission = drafts.begin("bot-a", "message for A");
8
+ drafts.setDraft("bot-b", "message for B");
9
+
10
+ expect(drafts.reject(submission)).toBe("message for A");
11
+ expect(drafts.draftFor("bot-a")).toBe("message for A");
12
+ expect(drafts.draftFor("bot-b")).toBe("message for B");
13
+ });
14
+
15
+ test("does not restore an older submission over a newer one for the same Bot", () => {
16
+ const drafts = new ComposerDraftStore();
17
+ const stale = drafts.begin("bot-a", "older");
18
+ const current = drafts.begin("bot-a", "newer");
19
+
20
+ expect(drafts.reject(stale)).toBeUndefined();
21
+ expect(drafts.reject(current)).toBe("newer");
22
+ });
23
+
24
+ test("preserves text typed after submission alongside rejected text", () => {
25
+ const drafts = new ComposerDraftStore();
26
+ const submission = drafts.begin("bot-a", "rejected");
27
+ drafts.setDraft("bot-a", "new draft");
28
+
29
+ expect(drafts.reject(submission)).toBe("rejected\n\nnew draft");
30
+ });
31
+ });
@@ -0,0 +1,35 @@
1
+ export interface ComposerSubmissionToken {
2
+ generation: number;
3
+ context: unknown;
4
+ text: string;
5
+ }
6
+
7
+ export class ComposerDraftStore {
8
+ readonly #drafts = new Map<unknown, string>();
9
+ readonly #generations = new Map<unknown, number>();
10
+
11
+ draftFor(context: unknown): string {
12
+ return this.#drafts.get(context) ?? "";
13
+ }
14
+
15
+ setDraft(context: unknown, draft: string): void {
16
+ this.#drafts.set(context, draft);
17
+ }
18
+
19
+ begin(context: unknown, text: string): ComposerSubmissionToken {
20
+ const generation = (this.#generations.get(context) ?? 0) + 1;
21
+ this.#generations.set(context, generation);
22
+ this.#drafts.set(context, "");
23
+ return { generation, context, text };
24
+ }
25
+
26
+ reject(token: ComposerSubmissionToken): string | undefined {
27
+ if (this.#generations.get(token.context) !== token.generation) {
28
+ return undefined;
29
+ }
30
+ const existing = this.draftFor(token.context);
31
+ const restored = existing ? `${token.text}\n\n${existing}` : token.text;
32
+ this.#drafts.set(token.context, restored);
33
+ return restored;
34
+ }
35
+ }
@@ -0,0 +1,15 @@
1
+ import type { Component, Ref } from "vue";
2
+
3
+ export interface Context {
4
+ client: {
5
+ router: {
6
+ slot(options: {
7
+ type: string;
8
+ order?: number;
9
+ component: Component;
10
+ }): void;
11
+ };
12
+ };
13
+ }
14
+
15
+ export declare function useRpc<T>(): Ref<T>;