@promptbook/cli 0.112.0-123 → 0.112.0-124

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 (32) hide show
  1. package/README.md +14 -14
  2. package/agents/default/developer.book +23 -0
  3. package/apps/agents-server/src/app/api/scrape/route.ts +18 -0
  4. package/apps/agents-server/src/tools/createAgentProgressTools.ts +6 -4
  5. package/apps/agents-server/src/utils/assertSafeUrl.ts +136 -0
  6. package/apps/agents-server/src/utils/authenticateUser.ts +2 -1
  7. package/apps/agents-server/src/utils/getCurrentUser.ts +2 -1
  8. package/apps/agents-server/src/utils/isAdminPasswordEqual.ts +28 -0
  9. package/apps/agents-server/src/utils/isUserGlobalAdmin.ts +3 -1
  10. package/apps/agents-server/src/utils/userChat/createRunUserChatJobPersistenceController.ts +21 -0
  11. package/apps/agents-server/src/utils/userChat/retryUserChatJob.ts +4 -0
  12. package/apps/agents-server/src/utils/userChat/userChatMessageLifecycle.ts +2 -0
  13. package/apps/agents-server/src/utils/userChat/userChatProgressCard.ts +288 -0
  14. package/esm/index.es.js +78 -7
  15. package/esm/index.es.js.map +1 -1
  16. package/esm/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.d.ts +25 -0
  17. package/esm/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +2 -0
  18. package/esm/src/version.d.ts +1 -1
  19. package/package.json +1 -1
  20. package/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.ts +79 -0
  21. package/src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.ts +1 -1
  22. package/src/cli/cli-commands/coder/init.ts +2 -0
  23. package/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.ts +10 -0
  24. package/src/cli/cli-commands/coder/printInitializationSummary.ts +3 -0
  25. package/src/other/templates/getTemplatesPipelineCollection.ts +707 -876
  26. package/src/version.ts +2 -2
  27. package/src/versions.txt +1 -0
  28. package/umd/index.umd.js +77 -6
  29. package/umd/index.umd.js.map +1 -1
  30. package/umd/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.d.ts +25 -0
  31. package/umd/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +2 -0
  32. package/umd/src/version.d.ts +1 -1
package/README.md CHANGED
@@ -541,7 +541,7 @@ Prompts marked with `[-]` are not ready yet, prompts containing `@@@` are treate
541
541
  #### Features
542
542
 
543
543
  - **Multi-runner execution:** `openai-codex`, `github-copilot`, `cline`, `claude-code`, `opencode`, `gemini`
544
- - **Context injection:** `--context AGENTS.md` or inline extra instructions
544
+ - **Context injection:** `--agent agents/coding/developer.book --context AGENTS.md` or inline extra instructions
545
545
  - **Reasoning control:** `--thinking-level low|medium|high|xhigh` for supported runners
546
546
  - **Interactive or unattended runs:** default wait mode, or `--no-wait` for batch execution
547
547
  - **Git safety:** clean working tree check by default, optional `--ignore-git-changes`
@@ -561,11 +561,11 @@ npx ts-node ./src/cli/test/ptbk.ts coder generate-boilerplates --template prompt
561
561
 
562
562
  npx ts-node ./src/cli/test/ptbk.ts coder generate-boilerplates --template prompts/templates/agents-server.md
563
563
 
564
- npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md
564
+ npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md
565
565
 
566
- npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --auto-push
566
+ npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md --auto-push
567
567
 
568
- npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --ignore-git-changes --no-wait
568
+ npx ts-node ./src/cli/test/ptbk.ts coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md --ignore-git-changes --no-wait
569
569
 
570
570
  npx ts-node ./src/cli/test/ptbk.ts coder find-refactor-candidates
571
571
 
@@ -587,11 +587,11 @@ ptbk coder generate-boilerplates
587
587
 
588
588
  ptbk coder generate-boilerplates --template prompts/templates/common.md
589
589
 
590
- ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --test npm run test
590
+ ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md --test npm run test
591
591
 
592
- ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --auto-push
592
+ ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md --auto-push
593
593
 
594
- ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --context AGENTS.md --test npm run test --ignore-git-changes --no-wait
594
+ ptbk coder run --harness github-copilot --model gpt-5.4 --thinking-level xhigh --agent agents/coding/developer.book --context AGENTS.md --test npm run test --ignore-git-changes --no-wait
595
595
 
596
596
  ptbk coder find-refactor-candidates
597
597
 
@@ -604,13 +604,13 @@ ptbk coder verify
604
604
 
605
605
  #### What each command does
606
606
 
607
- | Command | What it does |
608
- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | ------ | ---- | ----- | ------------------------------------------------------------------------ |
607
+ | Command | What it does |
608
+ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | ------ | ---- | ----- | ------------------------------------------------------------------------ |
609
609
  | `ptbk coder init` | Creates `prompts/`, `prompts/done/`, the project-generic template files materialized in `prompts/templates/` (currently `common.md`), and a starter `AGENTS.md`; ensures `.env` contains `CODING_AGENT_GIT_NAME`, `CODING_AGENT_GIT_EMAIL`, and `CODING_AGENT_GIT_SIGNING_KEY`; adds helper coder scripts to `package.json`; ensures `.gitignore` contains `/.promptbook`; and configures `.vscode/settings.json` to save pasted prompt images into `prompts/screenshots/`. |
610
- | `ptbk coder generate-boilerplates` | Creates new prompt markdown files with fresh emoji tags so you can quickly fill in coding tasks; `--template` accepts either a built-in alias or a markdown file path relative to the project root. |
611
- | `ptbk coder run` | Picks the next ready prompt, appends optional context, runs it through the selected coding agent, can optionally verify each attempt with a shell test command and feed failing output back for retries, then marks success or failure, commits the result, and pushes only when `--auto-push` is enabled. |
612
- | `ptbk coder find-refactor-candidates` | Scans the repository for oversized or overpacked files and writes prompt files for likely refactors; `--level <xlow | low | medium | high | xhigh | extreme>` ranges from a very benevolent scan to a very aggressive sweep. |
613
- | `ptbk coder verify` | Walks through completed prompts, archives truly finished work, and adds follow-up repair prompts for unfinished results. |
610
+ | `ptbk coder generate-boilerplates` | Creates new prompt markdown files with fresh emoji tags so you can quickly fill in coding tasks; `--template` accepts either a built-in alias or a markdown file path relative to the project root. |
611
+ | `ptbk coder run` | Picks the next ready prompt, appends optional context, runs it through the selected coding agent, can optionally verify each attempt with a shell test command and feed failing output back for retries, then marks success or failure, commits the result, and pushes only when `--auto-push` is enabled. |
612
+ | `ptbk coder find-refactor-candidates` | Scans the repository for oversized or overpacked files and writes prompt files for likely refactors; `--level <xlow | low | medium | high | xhigh | extreme>` ranges from a very benevolent scan to a very aggressive sweep. |
613
+ | `ptbk coder verify` | Walks through completed prompts, archives truly finished work, and adds follow-up repair prompts for unfinished results. |
614
614
 
615
615
  #### Most useful `ptbk coder run` flags
616
616
 
@@ -633,7 +633,7 @@ ptbk coder verify
633
633
 
634
634
  1. Initialize once with `ptbk coder init`.
635
635
  2. Customize `prompts/templates/*.md` if needed, then create or write prompt files in `prompts/`.
636
- 3. Customize the starter `AGENTS.md` with repository-specific instructions, then pass `--context AGENTS.md`.
636
+ 3. Customize the starter `AGENTS.md` with repository-specific instructions, then pass `--agent agents/coding/developer.book --context AGENTS.md`.
637
637
  4. Run one prompt at a time interactively, or use `--no-wait` for unattended batches.
638
638
  5. Finish with `ptbk coder verify` so resolved prompts are archived and broken ones get explicit repair follow-ups.
639
639
 
@@ -0,0 +1,23 @@
1
+ Promptbook Developer
2
+
3
+ RULE
4
+ Keep in mind the DRY _(don't repeat yourself)_ principle.
5
+
6
+ RULE
7
+ Keep in mind the SOLID principles.
8
+
9
+ RULE
10
+ Do a proper analysis of the current functionality before you start implementing.
11
+
12
+ RULE
13
+ Keep small responsibilities of functions and classes, avoid creating big functions or classes that do many things.
14
+
15
+ RULE
16
+ Constants should always be `UPPER_SNAKE_CASE`.
17
+
18
+ RULE
19
+ Boolean variables should always be prefixed with `is`, for example `isUserChatJobLeaseExpired` or `IS_DEBUG_MODE`.
20
+
21
+ RULE
22
+ Do not use abbreviations, for example use `isExpired` instead of `isExp`, `translateMessage` instead of `t`, etc.
23
+ It is fine to use well-known abbreviations, for example `id`, `url`, `html`, etc.
@@ -2,6 +2,8 @@ import { serializeError } from '@promptbook-local/utils';
2
2
  import { NextRequest, NextResponse } from 'next/server';
3
3
  import { fetchUrlContent } from '../../../../../../src/commitments/USE_BROWSER/fetchUrlContent';
4
4
  import { assertsError } from '../../../../../../src/errors/assertsError';
5
+ import { getCurrentUser } from '../../../utils/getCurrentUser';
6
+ import { assertSafeUrl } from '../../../utils/assertSafeUrl';
5
7
 
6
8
  /**
7
9
  * API endpoint for scraping URL content
@@ -9,9 +11,17 @@ import { assertsError } from '../../../../../../src/errors/assertsError';
9
11
  * This endpoint proxies the server-side scraping functionality to be accessible from the browser.
10
12
  * It uses the fetchUrlContent utility to fetch and convert web content to markdown.
11
13
  *
14
+ * Requires authentication to prevent unauthenticated SSRF abuse.
15
+ * The destination URL is validated against private/internal IP ranges before fetching.
16
+ *
12
17
  * @route GET /api/scrape?url=<url>
13
18
  */
14
19
  export async function GET(request: NextRequest) {
20
+ const currentUser = await getCurrentUser();
21
+ if (!currentUser) {
22
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
23
+ }
24
+
15
25
  try {
16
26
  // Get the URL parameter from the query string
17
27
  const searchParams = request.nextUrl.searchParams;
@@ -22,6 +32,14 @@ export async function GET(request: NextRequest) {
22
32
  return NextResponse.json({ error: 'URL parameter is required' }, { status: 400 });
23
33
  }
24
34
 
35
+ // Guard against SSRF: reject private/internal IPs and non-HTTP(S) schemes
36
+ try {
37
+ assertSafeUrl(url);
38
+ } catch (error) {
39
+ assertsError(error);
40
+ return NextResponse.json({ error: error.message, success: false }, { status: 400 });
41
+ }
42
+
25
43
  // Use the server-side fetchUrlContent utility
26
44
  const content = await fetchUrlContent(url);
27
45
 
@@ -22,11 +22,13 @@ export function createAgentProgressTools(
22
22
  {
23
23
  name: AGENT_PROGRESS_TOOL_NAME,
24
24
  description: spaceTrim(`
25
- Update to user while you work on a response.
25
+ Publish real-time progress to the user while you work on a response.
26
26
 
27
- - Use this to communicate only concise progress updates visible to the user.
28
- - Include what you are doing now, and what you will do next.
29
- - Be informative and make update before any chunk of work that might take more than a few seconds.
27
+ - Use this for concrete progress, not placeholder reassurance.
28
+ - Call it before any chunk of work that may take more than a few seconds.
29
+ - Update it when you start a step, call or finish a tool, learn a result, or decide the next step.
30
+ - Include what you are doing now, visible actions/results, and what you will do next.
31
+ - Keep updates concise and user-facing; do not expose hidden chain-of-thought.
30
32
  `),
31
33
  parameters: {
32
34
  type: 'object',
@@ -0,0 +1,136 @@
1
+ import { isIPv4, isIPv6 } from 'net';
2
+ import { spaceTrim } from 'spacetrim';
3
+
4
+ /**
5
+ * Private IPv4 CIDR ranges that must never be reached via the scrape proxy.
6
+ * Covers loopback, RFC 1918 private ranges, link-local (APIPA / cloud metadata),
7
+ * and the shared address space (RFC 6598).
8
+ */
9
+ const PRIVATE_IPV4_RANGES: ReadonlyArray<{ readonly network: string; readonly prefixBits: number }> = [
10
+ { network: '127.0.0.0', prefixBits: 8 }, // loopback
11
+ { network: '10.0.0.0', prefixBits: 8 }, // RFC 1918
12
+ { network: '172.16.0.0', prefixBits: 12 }, // RFC 1918
13
+ { network: '192.168.0.0', prefixBits: 16 }, // RFC 1918
14
+ { network: '169.254.0.0', prefixBits: 16 }, // link-local / AWS metadata (169.254.169.254)
15
+ { network: '0.0.0.0', prefixBits: 8 }, // "this" network
16
+ { network: '100.64.0.0', prefixBits: 10 }, // shared address space (RFC 6598)
17
+ ];
18
+
19
+ /**
20
+ * Converts an IPv4 address string to an unsigned 32-bit integer.
21
+ */
22
+ function ipv4ToUint32(ip: string): number {
23
+ return ip.split('.').reduce((accumulator, octet) => (accumulator << 8) + parseInt(octet, 10), 0) >>> 0;
24
+ }
25
+
26
+ /**
27
+ * Returns true when the given IPv4 address falls within any private/reserved range.
28
+ */
29
+ function isPrivateIPv4(ip: string): boolean {
30
+ const ipAsNumber = ipv4ToUint32(ip);
31
+ for (const { network, prefixBits } of PRIVATE_IPV4_RANGES) {
32
+ const networkMask = prefixBits === 0 ? 0 : (~((1 << (32 - prefixBits)) - 1)) >>> 0;
33
+ if ((ipAsNumber & networkMask) === (ipv4ToUint32(network) & networkMask)) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /**
41
+ * Returns true when the given IPv6 address falls within any private/reserved range.
42
+ * Covers loopback (::1), unspecified (::), IPv4-mapped addresses (::ffff:A.B.C.D),
43
+ * ULA (fc00::/7), and link-local (fe80::/10).
44
+ */
45
+ function isPrivateIPv6(ip: string): boolean {
46
+ const normalized = ip.toLowerCase();
47
+
48
+ if (normalized === '::1' || normalized === '::') {
49
+ return true;
50
+ }
51
+
52
+ // IPv4-mapped: ::ffff:A.B.C.D — delegate to the IPv4 check
53
+ if (normalized.startsWith('::ffff:')) {
54
+ const ipv4Part = normalized.slice('::ffff:'.length);
55
+ if (isIPv4(ipv4Part)) {
56
+ return isPrivateIPv4(ipv4Part);
57
+ }
58
+ }
59
+
60
+ const firstGroup = parseInt((normalized.split(':')[0] ?? '0') || '0', 16);
61
+
62
+ // ULA: fc00::/7 — first 7 bits are 1111110
63
+ if ((firstGroup & 0xfe00) === 0xfc00) {
64
+ return true;
65
+ }
66
+
67
+ // Link-local: fe80::/10 — first 10 bits are 1111111010
68
+ if ((firstGroup & 0xffc0) === 0xfe80) {
69
+ return true;
70
+ }
71
+
72
+ return false;
73
+ }
74
+
75
+ /**
76
+ * Asserts that a URL is safe to use as a scrape target, blocking SSRF attacks.
77
+ *
78
+ * Throws an error when:
79
+ * - The URL scheme is not `http:` or `https:`
80
+ * - The hostname is `localhost`
81
+ * - The hostname is a private/reserved IPv4 address (RFC 1918, loopback, link-local, …)
82
+ * - The hostname is a private/reserved IPv6 address (ULA, link-local, loopback, …)
83
+ *
84
+ * @throws {Error} when the URL would allow reaching an internal/private resource
85
+ */
86
+ export function assertSafeUrl(url: string): void {
87
+ let parsedUrl: URL;
88
+ try {
89
+ parsedUrl = new URL(url);
90
+ } catch {
91
+ throw new Error(`Invalid URL: \`${url}\``);
92
+ }
93
+
94
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
95
+ throw new Error(spaceTrim(`
96
+ URL scheme \`${parsedUrl.protocol}\` is not allowed.
97
+
98
+ Only \`http:\` and \`https:\` schemes are permitted.
99
+ `));
100
+ }
101
+
102
+ const hostname = parsedUrl.hostname.toLowerCase();
103
+
104
+ if (hostname === 'localhost') {
105
+ throw new Error(
106
+ spaceTrim(`
107
+ Requests to \`localhost\` are not allowed.
108
+
109
+ Only public, externally reachable URLs may be scraped.
110
+ `),
111
+ );
112
+ }
113
+
114
+ if (isIPv4(hostname) && isPrivateIPv4(hostname)) {
115
+ throw new Error(
116
+ spaceTrim(`
117
+ Requests to private IPv4 address \`${hostname}\` are not allowed.
118
+
119
+ Only public, externally reachable URLs may be scraped.
120
+ `),
121
+ );
122
+ }
123
+
124
+ // IPv6 addresses appear inside square brackets in URLs: http://[::1]/path
125
+ const ipv6Candidate = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
126
+
127
+ if (isIPv6(ipv6Candidate) && isPrivateIPv6(ipv6Candidate)) {
128
+ throw new Error(
129
+ spaceTrim(`
130
+ Requests to private IPv6 address \`${ipv6Candidate}\` are not allowed.
131
+
132
+ Only public, externally reachable URLs may be scraped.
133
+ `),
134
+ );
135
+ }
136
+ }
@@ -2,6 +2,7 @@ import { $getTableName } from '../database/$getTableName';
2
2
  import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
3
3
  import { AgentsServerDatabase } from '../database/schema';
4
4
  import { verifyPassword } from './auth';
5
+ import { isAdminPasswordEqual } from './isAdminPasswordEqual';
5
6
 
6
7
  /**
7
8
  * Type describing authenticated user.
@@ -17,7 +18,7 @@ export type AuthenticatedUser = {
17
18
  */
18
19
  export async function authenticateUser(username: string, password: string): Promise<AuthenticatedUser | null> {
19
20
  // 1. Check if it's the environment admin
20
- if (username === 'admin' && process.env.ADMIN_PASSWORD && password === process.env.ADMIN_PASSWORD) {
21
+ if (username === 'admin' && isAdminPasswordEqual(password)) {
21
22
  return { username: 'admin', isAdmin: true, isGlobalAdmin: true };
22
23
  }
23
24
 
@@ -3,6 +3,7 @@ import { cache } from 'react';
3
3
  import { $getTableName } from '../database/$getTableName';
4
4
  import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
5
5
  import type { AgentsServerDatabase } from '../database/schema';
6
+ import { isAdminPasswordEqual } from './isAdminPasswordEqual';
6
7
  import { getSession } from './session';
7
8
 
8
9
  /**
@@ -75,7 +76,7 @@ const getCachedCurrentUser = cache(async (): Promise<UserInfo | null> => {
75
76
  if (process.env.ADMIN_PASSWORD) {
76
77
  const cookieStore = await cookies();
77
78
  const adminToken = cookieStore.get('adminToken');
78
- if (adminToken?.value === process.env.ADMIN_PASSWORD) {
79
+ if (adminToken?.value !== undefined && isAdminPasswordEqual(adminToken.value)) {
79
80
  return {
80
81
  username: 'admin',
81
82
  isAdmin: true,
@@ -0,0 +1,28 @@
1
+ import { timingSafeEqual } from 'crypto';
2
+
3
+ /**
4
+ * Compares a candidate string against the `ADMIN_PASSWORD` environment variable
5
+ * using a constant-time algorithm to prevent timing attacks.
6
+ *
7
+ * Returning `false` early when the byte lengths differ does not expose character
8
+ * content — only length — which is an acceptable trade-off, since `timingSafeEqual`
9
+ * requires equal-length buffers and length is typically not secret.
10
+ *
11
+ * @param candidate The value to compare against the configured admin password.
12
+ * @returns `true` when `candidate` matches `ADMIN_PASSWORD`, `false` otherwise.
13
+ */
14
+ export function isAdminPasswordEqual(candidate: string): boolean {
15
+ const adminPassword = process.env.ADMIN_PASSWORD;
16
+ if (!adminPassword) {
17
+ return false;
18
+ }
19
+
20
+ const candidateBuffer = Buffer.from(candidate);
21
+ const adminPasswordBuffer = Buffer.from(adminPassword);
22
+
23
+ if (candidateBuffer.length !== adminPasswordBuffer.length) {
24
+ return false;
25
+ }
26
+
27
+ return timingSafeEqual(candidateBuffer, adminPasswordBuffer);
28
+ }
@@ -1,5 +1,6 @@
1
1
  import { cookies } from 'next/headers';
2
2
  import { cache } from 'react';
3
+ import { isAdminPasswordEqual } from './isAdminPasswordEqual';
3
4
  import { getSession } from './session';
4
5
 
5
6
  /**
@@ -18,7 +19,8 @@ const getCachedIsUserGlobalAdmin = cache(async (): Promise<boolean> => {
18
19
  }
19
20
 
20
21
  const cookieStore = await cookies();
21
- return cookieStore.get('adminToken')?.value === process.env.ADMIN_PASSWORD;
22
+ const adminToken = cookieStore.get('adminToken')?.value;
23
+ return adminToken !== undefined && isAdminPasswordEqual(adminToken);
22
24
  });
23
25
 
24
26
  /**
@@ -4,6 +4,7 @@ import type { ChatPromptResult } from '../../../../../src/execution/PromptResult
4
4
  import { mergeToolCalls } from '../../../../../src/utils/toolCalls/mergeToolCalls';
5
5
  import { persistUserChatJobTerminalState } from './persistUserChatJobTerminalState';
6
6
  import { updateUserChatAssistantMessage } from './updateUserChatAssistantMessage';
7
+ import { createRunningUserChatProgressCard } from './userChatProgressCard';
7
8
  import { USER_CHAT_JOB_ASSISTANT_MESSAGE_PERSIST_INTERVAL_MS } from './userChatJobRuntimeConstants';
8
9
  import type { UserChatJobRecord } from './UserChatJobRecord';
9
10
 
@@ -44,6 +45,14 @@ export function createRunUserChatJobPersistenceController(options: {
44
45
  let hasPendingAssistantMessageUpdate = false;
45
46
  let assistantMessageUpdateTimeout: ReturnType<typeof setTimeout> | null = null;
46
47
 
48
+ /**
49
+ * Creates a typed ISO timestamp for progress-card updates.
50
+ *
51
+ * @private function of `createRunUserChatJobPersistenceController`
52
+ */
53
+ const createProgressUpdatedAt = (): NonNullable<ChatMessage['progressCard']>['updatedAt'] =>
54
+ new Date().toISOString() as NonNullable<ChatMessage['progressCard']>['updatedAt'];
55
+
47
56
  /**
48
57
  * Queues one assistant-message mutation behind any earlier persistence work.
49
58
  *
@@ -132,6 +141,12 @@ export function createRunUserChatJobPersistenceController(options: {
132
141
  lifecycleState: 'running',
133
142
  lifecycleError: undefined,
134
143
  isComplete: false,
144
+ progressCard: createRunningUserChatProgressCard({
145
+ currentProgressCard: message.progressCard,
146
+ content: latestContent,
147
+ toolCalls: latestToolCalls,
148
+ updatedAt: createProgressUpdatedAt(),
149
+ }),
135
150
  prompt: options.createPromptSnapshot({
136
151
  toolCalls: latestToolCalls,
137
152
  }),
@@ -186,6 +201,12 @@ export function createRunUserChatJobPersistenceController(options: {
186
201
  lifecycleState: 'running',
187
202
  lifecycleError: undefined,
188
203
  isComplete: false,
204
+ progressCard: createRunningUserChatProgressCard({
205
+ currentProgressCard: message.progressCard,
206
+ content: latestContent,
207
+ toolCalls: latestToolCalls,
208
+ updatedAt: createProgressUpdatedAt(),
209
+ }),
189
210
  availableTools: options.availableTools,
190
211
  prompt: options.createPromptSnapshot(),
191
212
  }),
@@ -1,11 +1,13 @@
1
1
  import type { UserChatJobRecord } from './UserChatJobRecord';
2
2
  import type { UserChatJobRow } from './UserChatJobRow';
3
3
  import type { Json } from '@/src/database/schema';
4
+ import type { ChatMessage } from '@promptbook-local/types';
4
5
  import { withoutLocalUserChatJobMetadata } from '../localChatRunner/LocalUserChatJobMetadata';
5
6
  import { getUserChatJobById } from './getUserChatJobById';
6
7
  import { mapUserChatJobRow } from './mapUserChatJobRow';
7
8
  import { provideUserChatJobTable } from './provideUserChatJobTable';
8
9
  import { updateUserChatAssistantMessage } from './updateUserChatAssistantMessage';
10
+ import { createQueuedUserChatProgressCard } from './userChatProgressCard';
9
11
 
10
12
  /**
11
13
  * Requeues one failed durable chat job and resets its assistant placeholder.
@@ -23,6 +25,7 @@ export async function retryUserChatJob(jobId: string): Promise<UserChatJobRecord
23
25
  }
24
26
 
25
27
  const nowIso = new Date().toISOString();
28
+ const progressUpdatedAt = nowIso as NonNullable<ChatMessage['progressCard']>['updatedAt'];
26
29
  const userChatJobTable = await provideUserChatJobTable();
27
30
  const { data, error } = await userChatJobTable
28
31
  .update({
@@ -67,6 +70,7 @@ export async function retryUserChatJob(jobId: string): Promise<UserChatJobRecord
67
70
  completedToolCalls: undefined,
68
71
  prompt: undefined,
69
72
  generationDurationMs: undefined,
73
+ progressCard: createQueuedUserChatProgressCard(progressUpdatedAt),
70
74
  }),
71
75
  });
72
76
 
@@ -1,4 +1,5 @@
1
1
  import type { ChatMessage } from '../../../../../src/_packages/types.index';
2
+ import { createQueuedUserChatProgressCard } from './userChatProgressCard';
2
3
 
3
4
  /**
4
5
  * Lifecycle states rendered on canonical server-owned chat messages.
@@ -45,6 +46,7 @@ export function createQueuedUserChatAssistantMessage(options: {
45
46
  isComplete: false,
46
47
  lifecycleState: 'queued',
47
48
  jobId: options.jobId,
49
+ progressCard: createQueuedUserChatProgressCard(options.createdAt),
48
50
  };
49
51
  }
50
52