Просмотр исходного кода

Merge pull request #1173 from deepseek-harness/agent/web-bash-error-expand

fix(web): expand aborted Bash rows
imccyu 1 месяц назад
Родитель
Сommit
2d31ec5fee

+ 82 - 0
apps/web/tests/bash-abort-row.e2e.ts

@@ -0,0 +1,82 @@
+// Web e2e scenario: a cancelled Bash call can settle without terminal-card
+// material. Borrow the real cancellation fixture and prove the keyed Bash row
+// still exposes the recorded command and full error without any model call.
+import { readFile } from 'node:fs/promises'
+import { fileURLToPath } from 'node:url'
+import { join } from 'node:path'
+import type { Browser, Page } from 'playwright'
+import { chromium } from 'playwright'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import {
+  assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
+  launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
+} from './scaffold.ts'
+import { newEnglishPage, saveFailureShot } from './support.ts'
+
+const FIXTURE = fileURLToPath(new URL('../../../examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl', import.meta.url))
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/bash-abort-row', import.meta.url))
+const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
+const MODE = webSnapshotMode()
+const SEED_ID = 'bash-abort-row-web-e2e'
+const PROMPT = 'Run two shell commands: wait for cancellation, then write skipped.txt.'
+
+describe.skipIf(MODE === 'record')('web e2e: cancelled Bash row disclosure', () => {
+  let scaffold: WebScaffold
+  let browser: Browser
+  let page: Page
+  let tripwire: ReturnType<typeof watchConsole>
+
+  beforeAll(async () => {
+    const fixture = await readFile(FIXTURE, 'utf8')
+    expect(fixtureUserPrompts(fixture)).toEqual([PROMPT])
+    scaffold = await launchWebScaffold({})
+    await seedSession(scaffold, fixture, SEED_ID)
+    browser = await chromium.launch()
+    page = await newEnglishPage(browser)
+    tripwire = watchConsole(page)
+    await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
+    await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+
+    const groupRow = page.locator('[role="treeitem"]').first()
+    await groupRow.waitFor({ timeout: 15_000 })
+    await groupRow.click()
+    const sessionRow = page.locator('[role="treeitem"]').nth(1)
+    await sessionRow.waitFor({ timeout: 10_000 })
+    await sessionRow.click()
+    await page.locator('[data-sample="bash"]').nth(1).waitFor({ timeout: 15_000 })
+  }, 120_000)
+
+  afterAll(async () => {
+    await browser?.close()
+    await scaffold?.close()
+  })
+
+  it('expands the aborted row to its command and full error', async () => {
+    onTestFailed(() => saveFailureShot(page, 'web-e2e-bash-abort-row'))
+    const row = page.locator('[data-sample="bash"]').first()
+    const call = row.locator('xpath=..')
+    await expect.poll(() => row.getAttribute('aria-expanded')).toBe('false')
+    await expect.poll(() => call.getByText('Error: command aborted', { exact: true }).count()).toBe(1)
+    await row.click()
+
+    await expect.poll(() => row.getAttribute('aria-expanded')).toBe('true')
+    await call.getByText('IN', { exact: true }).waitFor()
+    await call.getByText('OUT', { exact: true }).waitFor()
+    await call.getByText('Wait until cancellation', { exact: false }).waitFor()
+    await call.getByText('setInterval(() => {}, 1000)', { exact: false }).waitFor()
+    await expect.poll(() => call.getByText('Error: command aborted', { exact: true }).count()).toBe(2)
+
+    const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
+      // The borrowed fixture's UTC date is still the previous day in PDT;
+      // the disclosure golden must not depend on the runner timezone.
+      .replace(/\b\d{1,2}\/\d{1,2}(?= \{\{clock\}\})/g, '{{date}}')
+      .split(SEED_ID).join('{{seededId}}')
+    await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
+    expect(tripwire.pageErrors).toEqual([])
+    expect(tripwire.warnings).toEqual([])
+  }, 60_000)
+
+  it('keeps its snapshot inventory closed', async () => {
+    await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
+  })
+})

+ 32 - 0
apps/web/tests/snapshots/bash-abort-row/ui.expected.md

@@ -0,0 +1,32 @@
+- banner:
+  - navigation "Session hierarchy":
+    - 'button "Run two shell commands: wait" [disabled]'
+  - tablist:
+    - tab "Chat" [selected]
+    - tab "Trajectory"
+- text: "Run two shell commands: wait for cancellation, then write skipped.txt. {{date}} {{clock}}"
+- button "Copy":
+  - img
+- button "Branch into a new conversation":
+  - img
+- button "Context injection":
+  - img
+  - img
+  - text: Context injection
+- 'button "Failed Bash Error: command aborted" [expanded]':
+  - img
+  - text: "Failed Bash Error: command aborted"
+- text: "IN { \"command\": \"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\", \"description\": \"Wait until cancellation\" } OUT Error: command aborted"
+- button "Inspect"
+- 'button "Failed Bash Error: tool call aborted before dispatch"':
+  - img
+  - text: "Failed Bash Error: tool call aborted before dispatch"
+- textbox "Message the agent"
+- button "Commands":
+  - img
+- 'button "Access mode, current: Workspace Write"': Workspace Write
+- button "Select model, current deepseek-v4-flash":
+  - text: deepseek-v4-flash
+  - img
+- button "Send message" [disabled]
+- text: 1 turns · 1 steps Tool call {{duration}} Cache hit 0% Input 10 tok · Output 10 tok

+ 2 - 1
apps/web/tsconfig.json

@@ -53,7 +53,8 @@
     "tests/shipped-composition.e2e.ts",
     "tests/goal-bar.e2e.ts",
     "tests/startup-auto-selection.e2e.ts",
-    "tests/subagent-conversation.e2e.ts"
+    "tests/subagent-conversation.e2e.ts",
+    "tests/bash-abort-row.e2e.ts"
   ],
   "references": [
     {

+ 2 - 2
packages/client/ui-conversation/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
-README.md: e610b990dd89204fd7e22e8b86f807d10b8ba439
-README.zh.md: 268e05a806db1468ba689608c178646af132fe25
+README.md: 728d67aaccee609d5f28ee8744c1f11ca74b040a
+README.zh.md: eec1d36b9758dddcf63eb75c3a74c51b4afb0a72

+ 1 - 1
packages/client/ui-conversation/README.md

@@ -20,7 +20,7 @@ A Think row stays collapsed by default and exposes live reasoning throughput wit
 
 Generic tool rows classify the built-in bash, read, search, write, edit, and run_code names into dedicated visual variants. The filesystem variants render the edit icon and a path summary; that path is a hover-underline link that opens the file with the host OS default application (`host.openPath`, relative paths resolve against the session cwd). Tool rows are not whole-row click targets and do not open the details panel. The code variant summarizes with the model-authored `description` and expands to the program itself; its logged sub-dispatches render as always-visible nested rows through the SAME keyed toolview hole (custom registrations and the GenericToolCard fallback apply to sub-rows unchanged). Cordis lifecycle tools reuse those generic variants while presenting `Inspect`, `Mount temporary Plugin`, and `Unmount temporary Plugin` with a shared Cordis accent; mount keeps the code variant's expandable source rendering.
 
-A tool call declaring the `terminal` render intent renders its command output inline, at both conversation render sites, through ui-primitives' `TerminalBlock`. `contract/terminal-card-model.ts` is the single derivation from the snapshot's `callView`/`resultView` pair, so the sites cannot disagree about a command, its cwd, or its exit status; it yields null — the generic path — for any other card tag, including one this client version does not know. Both sites therefore also show the card's run-state dot, which is the same `StateDot` semantic a tool row's leading icon carries, so a row and its own card always agree about one command's state. A multi-line command gets one prompt row per line, with the dot marking the call once on the first row — the exit status is the whole call's, so a dot per line would claim a per-line outcome bash does not report. The keyed `BashRow` carries the card resident below its summary row; since tool rows are no longer details-panel click targets, the card's copy and expand controls are the row's only interactions. The render-site fallback row keeps the card behind its existing expand control. Rows cap at `CHAT_TERMINAL_MAX_LINES` (8) against the panel's 16, which is what keeps a summary surface bounded — the panel stays the single-call reading surface. Inline output is licensed per render intent — the terminal and web cards, each with its own bound; a generic tool's content remains panel-only ([decision](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md)).
+A tool call declaring the `terminal` render intent renders its command output inline, at both conversation render sites, through ui-primitives' `TerminalBlock`. `contract/terminal-card-model.ts` is the single derivation from the snapshot's `callView`/`resultView` pair, so the sites cannot disagree about a command, its cwd, or its exit status; it yields null — the generic path — for any other card tag, including one this client version does not know. Both sites therefore also show the card's run-state dot, which is the same `StateDot` semantic a tool row's leading icon carries, so a row and its own card always agree about one command's state. A multi-line command gets one prompt row per line, with the dot marking the call once on the first row — the exit status is the whole call's, so a dot per line would claim a per-line outcome bash does not report. The keyed `BashRow` carries the card resident below its summary row; since tool rows are no longer details-panel click targets, the card's copy and expand controls are the row's only interactions. The render-site fallback row keeps the card behind its existing expand control. Rows cap at `CHAT_TERMINAL_MAX_LINES` (8) against the panel's 16, which is what keeps a summary surface bounded — the panel stays the single-call reading surface. Inline output is licensed per render intent — the terminal and web cards, each with its own bound. A Bash execution failure that settles on the generic path instead exposes its original arguments and full error through the same bounded IN/OUT disclosure, while successful generic results such as a background-start acknowledgement remain summary-only ([decision](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md)).
 
 A tool call declaring the `web` render intent renders its web retrieval inline, at both conversation render sites, through ui-primitives' `WebBlock`. `contract/web-card-model.ts` is the single derivation from the snapshot's `resultView`, mirroring the terminal card, so the sites cannot disagree about what a web call shows; it yields null — the generic path — for a running call, a non-web result view, a generic result view, a `card` tag this client version does not know, or a web card whose `kind` this client version does not know (a newer host's value, which the wire cannot be trusted to be `search` or `fetch`). The keyed `WebRow` registers one component under both `web_search` and `web_fetch`, discriminating on the tool name only for its icon and title; it composes the shared `ToolRow`, feeding the card as ToolRow's `web` body, so the retrieval is the row's collapsed-by-default expanded card (the same unified expand every card row has). A web-declaring tool without a keyed row lands on the `GenericToolCard` fallback, which routes the card through ToolRow the same way, and the details panel renders it at the primitive's full source allowance and, below the card, the flattened model-visible result content — a fetch body is readable only there, since its card carries only the URL and status. Rows cap at `CHAT_WEB_MAX_SOURCES` (8) against the panel's 16, the same summary-versus-reading split the terminal card draws ([decision](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.md)).
 

+ 1 - 1
packages/client/ui-conversation/README.zh.md

@@ -18,7 +18,7 @@ Think 行默认保持折叠,并在不展开思维链的情况下暴露实时
 
 通用工具行把内置的 bash、read、search、write、edit 和 run_code 名称归入专用视觉变体。文件系统变体会渲染 edit 图标和路径摘要;该路径是悬停下划线链接,点击后通过宿主操作系统的默认应用打开文件(`host.openPath`,相对路径相对会话 cwd 解析)。工具行不再是整行点击目标,也不会打开 details 面板。code 变体以模型撰写的 `description` 作摘要,展开后显示程序本身;其已记录的子调用经由同一个键控 toolview 空位渲染为始终可见的嵌套行(自定义注册和 GenericToolCard fallback 原样适用于子行)。Cordis 生命周期工具复用这些通用变体,同时以统一的 Cordis 强调色呈现 `Inspect`、`Mount temporary Plugin` 和 `Unmount temporary Plugin`;mount 行保留 code 变体的可展开源码渲染。
 
-声明 `terminal` 渲染意图的工具调用,会在两个对话渲染点上都通过 ui-primitives 的 `TerminalBlock` 内联渲染其命令输出。`contract/terminal-card-model.ts` 是从快照的 `callView`/`resultView` 对推导的唯一位置,因此两个渲染点不可能在命令、cwd 或退出状态上产生分歧;对任何其他 card 标签——包括当前客户端版本不认识的标签——它返回 null,落回通用路径。因此两个渲染点也都显示卡片的运行状态点,它与工具行行首图标承载同一套 `StateDot` 语义,所以一行与其自身的卡片对同一条命令的状态总是一致。多行命令的每一行各占一个提示行,状态点只在第一行为整次调用标记一次——退出状态属于整次调用,因此每行一枚就会声称一个 bash 并不报告的逐行结果。键控的 `BashRow` 把卡片常驻在摘要行下方;由于工具行已不再是详情面板的点击目标,卡片的复制与展开控件就是该行唯一的交互。渲染点兜底行则保持其既有的展开控件。行的上限是 `CHAT_TERMINAL_MAX_LINES`(8),面板为 16,正是这一点让摘要面保持有界——面板仍是单次调用的阅读面。内联输出按渲染意图开放——终端卡片与 web 卡片,各有自己的上限;通用工具的内容仍然只在面板中呈现([决策](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md))。
+声明 `terminal` 渲染意图的工具调用,会在两个对话渲染点上都通过 ui-primitives 的 `TerminalBlock` 内联渲染其命令输出。`contract/terminal-card-model.ts` 是从快照的 `callView`/`resultView` 对推导的唯一位置,因此两个渲染点不可能在命令、cwd 或退出状态上产生分歧;对任何其他 card 标签——包括当前客户端版本不认识的标签——它返回 null,落回通用路径。因此两个渲染点也都显示卡片的运行状态点,它与工具行行首图标承载同一套 `StateDot` 语义,所以一行与其自身的卡片对同一条命令的状态总是一致。多行命令的每一行各占一个提示行,状态点只在第一行为整次调用标记一次——退出状态属于整次调用,因此每行一枚就会声称一个 bash 并不报告的逐行结果。键控的 `BashRow` 把卡片常驻在摘要行下方;由于工具行已不再是详情面板的点击目标,卡片的复制与展开控件就是该行唯一的交互。渲染点兜底行则保持其既有的展开控件。行的上限是 `CHAT_TERMINAL_MAX_LINES`(8),面板为 16,正是这一点让摘要面保持有界——面板仍是单次调用的阅读面。内联输出按渲染意图开放——终端卡片与 web 卡片,各有自己的上限。若 Bash 执行失败时落在通用路径,则改用同样有界的 IN/OUT 展开区暴露原始参数和完整错误;后台启动确认等成功的通用结果仍只显示摘要([决策](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md))。
 
 声明 `web` 渲染意图的工具调用,会在两个对话渲染点上都通过 ui-primitives 的 `WebBlock` 内联渲染其 web 检索。`contract/web-card-model.ts` 是从快照的 `resultView` 推导的唯一位置,镜像终端卡片,因此两个渲染点不可能对一次 web 调用的显示产生分歧;对运行中的调用、非 web 的 result view、generic result view、本客户端版本不认识的 `card` 标签,或本客户端版本不认识 `kind` 的 web 卡片(更新的 host 发来的值,wire 上不可信其为 `search` 或 `fetch`),它返回 null,落回通用路径。键控的 `WebRow` 把一个组件注册在 `web_search` 与 `web_fetch` 两个键下,仅根据工具名判别以选取图标与标题;它组合共享的 `ToolRow`,把卡片作为 ToolRow 的 `web` body 传入,因此检索成为该行默认折叠的展开卡片(与每个卡片行相同的统一展开交互)。没有自己键控行的 web 声明工具落到 `GenericToolCard` 兜底,它以同样方式经 ToolRow 渲染卡片,详情面板则以原语的完整 source 额度渲染它,并在卡片下方渲染摊平的模型可见结果内容——fetch 正文只在此处可读,因为其卡片只携带 URL 和状态。行的上限是 `CHAT_WEB_MAX_SOURCES`(8),面板为 16,与终端卡片所画的摘要面对阅读面的同一划分([决策](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.md))。
 

+ 57 - 0
packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css

@@ -20,6 +20,63 @@
   border: 1px solid var(--dsw-alias-border-l1);
 }
 
+/* A bash execution error can settle without terminal-card material (for
+   example, command cancellation). Preserve ToolRow's bounded IN/OUT fallback
+   so the original command and full error remain available from this keyed row. */
+.ioCard {
+  display: flex;
+  flex-direction: column;
+  margin: 4px 0 4px 4px;
+  border: 1px solid var(--dsw-alias-border-l1);
+  border-radius: 12px;
+  background: var(--dsw-alias-markdown-code-block);
+  font: var(--dsw-font-markdown-code-block-small);
+}
+
+.ioSection {
+  display: grid;
+  grid-template-columns: max-content 1fr;
+  column-gap: 14px;
+  align-items: baseline;
+  padding: 12px 16px;
+  max-height: 150px;
+  overflow-y: auto;
+}
+
+.ioSection::-webkit-scrollbar-thumb {
+  border: 2px solid transparent;
+  background-clip: padding-box;
+  border-radius: 6px;
+}
+
+.ioSection::-webkit-scrollbar-track {
+  margin: 6px 0;
+}
+
+.ioLabel {
+  position: sticky;
+  top: 0;
+  align-self: start;
+  color: var(--dsw-alias-label-caption);
+}
+
+.ioDivider {
+  flex: none;
+  height: 1px;
+  background: var(--dsw-alias-border-l2);
+}
+
+.ioText {
+  min-width: 0;
+  white-space: pre-wrap;
+  word-break: break-word;
+  color: var(--dsw-alias-label-secondary);
+}
+
+.ioText[data-error] {
+  color: var(--dsw-alias-state-error-primary);
+}
+
 /* ToolRow's unified expand interaction, replicated per the registrant
    posture: pointer on the expandable row (the icon→chevron hover preview is
    the affordance, no row fill). */

+ 43 - 11
packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx

@@ -2,8 +2,10 @@
 // (ctx.slots.register + ToolRowProps only — never imports the chat domain).
 // Product chrome matches ToolRow / Think (figma: Bash · {description}).
 //
-// A bash call declares the terminal render intent, so this row renders the
-// command's own output through TerminalBlock — expand-gated exactly like
+// A bash call normally declares the terminal render intent, so this row renders
+// the command's own output through TerminalBlock. Execution failures that
+// settle without terminal material use the bounded generic IN/OUT fallback —
+// both are expand-gated exactly like
 // ToolRow's unified interaction: collapsed by default, the whole summary row
 // is the toggle (click / Enter / Space, icon→chevron hover preview; the
 // summary stays inline while open),
@@ -48,7 +50,7 @@ function stateStatus(state: ToolRowState, t: BashRowProps['t']): string | null {
 
 /**
  * Bash row: icon + Bash · {description} in the shared ToolRow chrome, the
- * whole row toggling the command's terminal card (ToolRow's unified
+ * whole row toggling the command's terminal or generic error card (ToolRow's unified
  * expand interaction, replicated locally per the registrant posture).
  */
 export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }: BashRowProps) {
@@ -64,7 +66,13 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }:
     : model.state
   const status = stateStatus(state, t)
   const [expanded, setExpanded] = useState(false)
-  const expandable = terminal !== null
+  // Execution failures (for example cancellation before the process reports a
+  // terminal result) use the generic presenter. Keep their recorded args and
+  // full error reachable instead of collapsing the row to the first line.
+  const genericError = terminal === null
+    && model.state === 'error'
+    && (model.body !== null || model.output !== null)
+  const expandable = terminal !== null || genericError
   const open = expanded && expandable
   const failureLine = model.state === 'error' ? model.errorSummary : null
   const toggleExpand = () => {
@@ -109,16 +117,40 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }:
           {failureLine ?? terminal?.description ?? model.summary}
         </span>
       </div>
-      {terminal !== null && open && (
+      {open && (
         /* Same hover-Inspect posture as ToolRow's expanded body, replicated
            locally per the registrant posture. */
         <div className={css.bodyWrap}>
-          <TerminalBlock
-            {...terminal.card}
-            maxLines={Infinity}
-            labels={terminalBlockLabels(t)}
-            className={css.terminal}
-          />
+          {terminal !== null
+            ? (
+              <TerminalBlock
+                {...terminal.card}
+                maxLines={Infinity}
+                labels={terminalBlockLabels(t)}
+                className={css.terminal}
+              />
+            )
+            : (
+              <div className={css.ioCard}>
+                {model.body !== null && (
+                  <div className={css.ioSection}>
+                    <span className={css.ioLabel}>IN</span>
+                    <span className={css.ioText}>{model.body}</span>
+                  </div>
+                )}
+                {model.body !== null && model.output !== null && (
+                  <span className={css.ioDivider} aria-hidden />
+                )}
+                {model.output !== null && (
+                  <div className={css.ioSection}>
+                    <span className={css.ioLabel}>OUT</span>
+                    <span className={css.ioText} data-error>
+                      {model.output}
+                    </span>
+                  </div>
+                )}
+              </div>
+            )}
           {inspect !== undefined && (
             <button type="button" className={css.inspectButton} onClick={inspect}>
               <svg width="12" height="12" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden>

+ 22 - 0
packages/client/ui-conversation/tests/terminal-card.spec.tsx

@@ -414,6 +414,28 @@ describe('BashRow terminal card', () => {
     }))} />)
     expect(view.getByText('List files')).toBeTruthy()
     expect(view.queryByText(/a\.ts/)).toBeNull()
+    expect(view.container.querySelector('[data-sample="bash"]')?.getAttribute('role')).toBeNull()
+  })
+
+  it('expands a generic execution error to its original args and full output', () => {
+    const view = render(<BashRow {...rowProps(settled({
+      content: [{ type: 'text', text: 'Error: command aborted' }],
+      isError: true,
+      callView: { card: 'generic', title: 'ls -la', kind: 'execute' },
+      resultView: { card: 'generic' },
+    }))} />)
+    const row = view.container.querySelector('[data-sample="bash"]')!
+    expect(row.getAttribute('role')).toBe('button')
+    expect(row.getAttribute('aria-expanded')).toBe('false')
+    expect(view.queryByText(/"command": "ls -la"/)).toBeNull()
+
+    fireEvent.click(row)
+
+    expect(row.getAttribute('aria-expanded')).toBe('true')
+    expect(view.getByText('IN')).toBeTruthy()
+    expect(view.getByText('OUT')).toBeTruthy()
+    expect(view.getByText(/"command": "ls -la"/)).toBeTruthy()
+    expect(view.container.querySelector('[data-error]')?.textContent).toBe('Error: command aborted')
   })
 })
 

+ 1 - 0
tsconfig.host.json

@@ -41,6 +41,7 @@
     "apps/web/tests/goal-bar.e2e.ts",
     "apps/web/tests/startup-auto-selection.e2e.ts",
     "apps/web/tests/subagent-conversation.e2e.ts",
+    "apps/web/tests/bash-abort-row.e2e.ts",
     "apps/cli/tests/**/*.ts",
     "examples/*/src/**/*.ts",
     "examples/*/start.ts",