SLOPSHOPPER

sencha-extjs

Sencha Ext JS Claude Code Mod

newpanebandrowsguardcommand
v0.7.0no licenseupdated 2026-10-15mgusmano/gusmano-marketplace/plugins/sencha-extjs
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · sencha-extjs
› fix the failing auth test and add an audit log call ● sencha-extjs: session.start: v0.0.18 At 10/16/2026, 2:20:49 PM ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /extjs ⎿ sencha-extjs: Sencha Ext JS pane opened Sencha Ext JS · Classic toolkit · Classic theme ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Sencha Ext JS · Classic toolkit · Classic theme ⟨Claude Code's own drawing⟩
Source 2 files
hooks/register.tsx 541 lines
1import type { Register, ToolCallInput } from 'claude-code'
2import { atom, read, update } from 'claude-code'
3import type { UpgradeRun, UpgradeStep, Message } from '../types'
4
5export type Turn = { seconds: number; tools: number }
6
7const VERSION_STRING = 'v0.0.18'
8
9const PANE = 'sencha-extjs'
10const PREFIX = 'mcp__extjs-tools__'
11const spaces = 0
12const ANSWER_PREFIX = 'Sencha AI Upgrade: '
13const LOGO_SVG = 'assets/extjs-logo.svg'
14const LOGO_PNG = 'assets/extjs-logo.png'
15
16const tail = (p: string, max = 46) => (p.length > max ? '…' + p.slice(-(max - 1)) : p)
17const str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v : undefined)
18const num = (v: unknown): number | undefined => (typeof v === 'number' ? v : undefined)
19
20const status = atom({ plugin: 'sencha-extjs', key: 'status' } as const, [] as Message[])
21const run = atom({ plugin: 'sencha-extjs', key: 'run' } as const, {})
22const steps = atom({ plugin: 'sencha-extjs', key: 'steps' } as const, [])
23const messages = atom({ plugin: 'sencha-extjs', key: 'messages' } as const, [] as Message[])
24//const last = atom({ plugin: 'turn-band', key: 'last' } as const, null)
25//const isHidden = atom({ plugin: 'turn-band', key: 'isHidden' } as const, false)
26
27// The extjs-tools results are a prose line followed by one or more JSON
28// objects, so take the last object that parses rather than the first brace.
29export function lastJson(text: string | undefined): Record<string, unknown> | undefined {
30  if (!text) return undefined
31  const end = text.lastIndexOf('}')
32  if (end < 0) return undefined
33  let found: Record<string, unknown> | undefined
34  let tries = 0
35  for (let i = text.indexOf('{'); i >= 0 && i <= end && tries < 60; i = text.indexOf('{', i + 1)) {
36    tries++
37    try {
38      const value: unknown = JSON.parse(text.slice(i, end + 1))
39      if (value && typeof value === 'object' && !Array.isArray(value)) {
40        found = value as Record<string, unknown>
41        break
42      }
43    } catch {
44      // not an object boundary; try the next brace
45    }
46  }
47  return found
48}
49
50export function absorb(tool: string, text: string | undefined, prev: UpgradeRun): UpgradeRun {
51  if (tool === 'sencha-stop') return { ...prev, serverUrl: undefined }
52  if (tool === 'sencha-web-start' || tool === 'sencha-app-watch') {
53    const url = (text ?? '').match(/https?:\/\/[^\s"']+/)
54    return { ...prev, serverUrl: url ? url[0] : prev.serverUrl }
55  }
56  const d = lastJson(text)
57  if (!d) return prev
58  const lint = (d.content && typeof d.content === 'object' ? d.content : d) as Record<string, unknown>
59  return {
60    ...prev,
61    projectType: str(d.projectType) ?? prev.projectType,
62    toolkit: str(d.toolkit) ?? prev.toolkit,
63    theme: str(d.theme) ?? prev.theme,
64    sourceVersion: str(d.sourceVersion) ?? prev.sourceVersion,
65    sourceRoot: str(d.sourceRoot) ?? str(d.sourceFolderPath) ?? prev.sourceRoot,
66    targetRoot: str(d.targetRoot) ?? prev.targetRoot,
67    lintFolders: num(lint.foldersProcessed) ?? prev.lintFolders,
68    lintFilesFixed: num(lint.filesFixed) ?? prev.lintFilesFixed,
69    lintFoldersFailed: num(lint.foldersFailed) ?? prev.lintFoldersFailed,
70  }
71}
72
73// The one argument that says what a built-in call is about: the file, folder,
74// command or query the tool acts on, for the pane's TOOL CALLS rows.
75export function subjectOf(e: ToolCallInput): string | undefined {
76  switch (e.tool) {
77    case 'Bash':
78      return e.command
79    case 'Read':
80    case 'Edit':
81    case 'Write':
82      return e.file_path
83    case 'NotebookEdit':
84      return e.notebook_path
85    case 'ToolSearch':
86    case 'WebSearch':
87      return e.query
88    case 'WebFetch':
89      return e.url
90    default:
91      return undefined
92  }
93}
94
95let tool_use_id = '';
96
97export const register: Register = on => {
98  let startedAt = 0
99  let tools = 0
100  let prefixedAnswer = false
101  let hasCommandMessage = false
102  let isExtjsPrompt = false
103  let isReExtPrompt = false
104  let taskId: string | undefined
105
106  on('session.start', async ($, e, next) => {
107    $.ui.log(`session.start: ${VERSION_STRING} At ${new Date().toLocaleString()}`)
108    await $.command.register({ name: 'extjs', description: 'Open the Sencha Ext JS side pane' })
109    $.clock.after(500, () => {
110      //void update($, messages, list => [...list, { title: 'session.start:timer', message: '500ms elapsed' }])
111      void $.ui.open({ id: PANE, title: 'Sencha Ext JS' })
112    })
113    await update($, messages, list => [...list, { title: 'session.start', message: `At ${new Date().toLocaleString()}` }]);
114    return next(e)
115  })
116
117
118
119  on('session.end', async ($, e, next) => {
120    await update($, messages, list => [...list, { title: 'session.end', message: JSON.stringify(e, null, spaces) }]);
121    // /clear starts a new conversation in the same process: its cache is a new one
122    if (e.reason === 'clear') {
123      // samples = []
124      // lastKey = ''
125      // toastedFor = 0
126      // observed = .log
127      // ttl = baseTtl
128      // ttlSource = envSource
129      $.ui.invalidate('ui.render')
130      return next(e)
131    }
132    //timer?.cancel()
133    //timer = undefined
134    return next(e)
135  })
136
137  on('command.run', { command: 'extjs' }, async $ => {
138    await update($, messages, list => [...list, { title: 'command.run', message: `command 'extjs' executed.` }]);
139    await $.ui.open({ id: PANE, title: 'Sencha Ext JS' })
140    return { text: 'Sencha Ext JS pane opened' }
141  })
142
143  on('ui.close', async ($, e, next) => {
144    await update($, messages, list => [...list, { title: 'ui.close', message: JSON.stringify(e, null, spaces) }]);
145    if (e.id !== PANE) return next(e)
146    //isPaneOpen = false
147    return next(e)
148  })
149
150  on('ui.press', async ($, e, next) => {
151    //await update($, messages, list => [...list, { title: 'ui.press', message: JSON.stringify(e, null, spaces) }]);
152    if (e.plugin !== $.plugin.name || e.requestId !== PANE) return next(e)
153    if (e.element === 'close') await $.ui.close({ id: PANE }).catch(() => undefined)
154    return next(e)
155  })
156
157  // on('skill.prompt', async ($, e, next) => {
158  //   await update($, messages, l => [...l, { title: 'skill.prompt', message: e.skill }])
159  //   return next(e)
160  // })
161
162  on('prompt.submit', async ($, e, next) => {
163    await update($, messages, list => [...list, { title: 'prompt.submit', message: JSON.stringify(e, null, spaces) }]);
164    //$.ui.status(`status: Started At ${startedAt}`)
165    //$.ui.toast(`toast: Started At ${startedAt}`)
166    //$.ui.log(`log: Started At ${startedAt}`)
167    return next(e)
168  })
169
170  on('turn.start', async ($, e, next) => {
171    //await update($, messages, list => [...list, { title: 'turn.start', message: JSON.stringify(e, null, spaces) }]);
172    const begin = e.text.indexOf('<command-message>')
173    const end = e.text.indexOf('</command-message>')
174    hasCommandMessage = begin >= 0 && end > begin
175    const inner = hasCommandMessage ? e.text.slice(begin + 17, end).trim() : ''
176    isExtjsPrompt = inner.startsWith('extjs')
177    isReExtPrompt = inner.startsWith('reext')
178    await update($, messages, list => [...list, { title: 'turn.start', message: `command: ${String(hasCommandMessage)} · extjs: ${String(isExtjsPrompt)} · reext: ${String(isReExtPrompt)} · ${JSON.stringify(inner.slice(0, 60))}` }]);
179    startedAt = await $.clock.now()
180    tools = 0
181    prefixedAnswer = false
182    return next(e)
183  })
184
185  on('turn.step', async function* ($, e, next) {
186    //await update($, messages, list => [...list, { title: 'turn.step', message: JSON.stringify(e, null, spaces) }]);
187    await update($, messages, list => [...list, { title: 'turn.step', message: JSON.stringify({ index: e.index, messageCount: e.messageCount }, null, spaces) }]);
188    // The shown text is the concatenation of the `text` chunks, so rewrite the
189    // first one to prefix the answer. Every step is its own model request, so
190    // anything that happened between steps (a file read, an MCP call) does not
191    // let an earlier prefix stand: this step's first text run earns its own.
192    // Within a step, a tool call (or the thinking and arguments around it)
193    // breaks the answer text, so the text after it is a new run and is
194    // prefixed too. `engine` chunks are the envelope, not a break, and are
195    // ignored so a single streamed text block is never split into two prefixes.
196    // Never re-prefix text that already carries the prefix, so a stacked or
197    // replayed hook cannot write it more than once.
198    prefixedAnswer = false
199    for await (const c of next(e)) {
200      if (c.kind !== 'text' && c.kind !== 'engine') prefixedAnswer = false
201      if (c.kind === 'text' && c.text.length > 0 && !prefixedAnswer && !c.text.startsWith(ANSWER_PREFIX)) {
202        prefixedAnswer = true
203        yield { ...c, text: `${ANSWER_PREFIX}${c.text}` }
204      } else {
205        yield c
206        //yield { ...c, text: `plain: ` }
207      }
208    }
209  })
210
211  on('turn.complete', async ($, e, next) => {
212    await update($, messages, list => [...list, { title: 'turn.complete', message: JSON.stringify(e, null, spaces) }]);
213    const seconds = Math.round(((await $.clock.now()) - startedAt) / 1000)
214    const dur = `${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`
215    const turn: Turn = { seconds, tools }
216    //await update($, messages, list => [...list, { title: 'turn.complete', message: JSON.stringify({ usage: e.usage, seconds, tools }, null, spaces) }]);
217    await update($, messages, list => [...list, { title: 'turn.complete', message: `${dur} ${tool_use_id}` }]);
218    const result = await next(e)
219    return result;
220    //return { ...result, text: `That turn took ${seconds}s` }
221  })
222
223  on('tool.call', async ($, e, next) => {
224    tool_use_id = e.tool_use_id;
225
226   await update($, messages, list => [...list, { title: 'tool.call', message: JSON.stringify(e, null, spaces) }]);
227    await update($, messages, list => [...list, { title: 'tool.call', message: e.tool }]);
228    await update($, messages, list => [...list, { title: 'tool.call', message: tool_use_id }]);
229    //if (!e.tool.startsWith(PREFIX)) return next(e)
230    const id = e.tool_use_id
231    const tool = e.tool.startsWith(PREFIX) ? e.tool.slice(PREFIX.length) : e.tool
232    const subject = subjectOf(e)
233    await update($, steps, list => [...list, { id, tool, subject, status: 'runs' as const }].slice(-60))
234    const ran = await next(e)
235    //await update($, messages, list => [...list, { title: 'tool.call:ran', message: JSON.stringify(ran, null, spaces) }]);
236    //await update($, messages, list => [...list, { title: 'tool', message: `tool: ${tool}` }]);
237
238    if (tool === 'job-status') {
239      const result = ran.result as Array<{ text?: string }> | undefined
240      const status: any = result?.[0]?.text
241      const statusJson = JSON.parse(status)
242      //await update($, messages, list => [...list, { title: 'status', message: JSON.stringify(result?.[0]?.text, null, spaces) }]);
243      await update($, messages, list => [...list, { title: 'status', message: `${statusJson.state}: ${statusJson.percent}% - ${statusJson.message}` }]);
244    }
245
246    const blocks = ran.result as Array<{ text?: string }> | undefined
247    const text = blocks?.[0]?.text ?? ''
248    if (tool === 'task-start') {
249      try {
250        taskId = (JSON.parse(text) as { taskId?: string }).taskId ?? taskId
251      } catch {
252        // the start result was not JSON; keep the previous task id
253      }
254      let statusMessage = (JSON.parse(text) as { statusMessage?: string }).statusMessage ?? ''
255      await update($, status, list => [...list, { title: 'task-start', message: statusMessage }])
256      await update($, messages, list => [...list, { title: 'task-start', message: text }])
257    }
258    if (tool === 'task-get' && taskId !== undefined && (e as { taskId?: string }).taskId === taskId) {
259      let statusMessage = (JSON.parse(text) as { statusMessage?: string }).statusMessage ?? ''
260      await update($, status, list => [...list, { title: 'task-get', message: statusMessage }])
261      await update($, messages, list => [...list, { title: 'task-get', message: statusMessage }])
262    }
263
264    const failed = ran.isError === true || ran.deny !== undefined
265    await update($, steps, list =>
266      list.map(one => (one.id === id ? { ...one, status: failed ? ('fail' as const) : ('done' as const) } : one)),
267    )
268    if (!failed) await update($, run, prev => absorb(tool, ran.text, prev))
269    return ran
270    // Bookkeeping only: never let a pane failure change what the tool does.
271  }).catch(($, e, next) => next(e))
272
273  // on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
274  //   const ran = await next({ ...e, command: e.command.trim() })
275  //   const hasFailed = ran.deny === undefined && ran.isError === true
276  //   $.ui.status(hasFailed ? `failed: ${e.command.slice(0, 40)}` : undefined)
277  //   return ran
278  // })
279
280  // // The matcher limits the hook to Bash calls, so e.command is the shell command
281  // on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
282  //   //if (/git push .*--force/.test(e.command)) {
283  //   if (/ls/.test(e.command)) {
284  //     // Returning without calling next answers the event, so the command never runs
285  //     // return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
286  //     return { deny: 'ls is not allowed.' }
287  //   }
288  //   // Every other command goes on to the permission check and then to Bash
289  //   return next(e)
290  // })
291
292  // on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
293  //   const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force/
294
295  //   // Let every other command through without a question
296  //   if (!RISKY.test(e.command)) return next(e)
297  //   // Start from the safe answer, so a question nobody answers refuses the command
298  //   let answer = 'Refuse'
299  //   try {
300  //     // The tool call waits here until the user picks one of the two labels
301  //     answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
302  //   } catch {
303  //     // The user dismissed the question, or this is a claude -p run with nobody to ask
304  //   }
305  //   if (answer !== 'Run it') {
306  //     // Answer without calling next, so the command doesn't run
307  //     return { deny: 'The user declined this command. Ask before trying a different approach.' }
308  //   }
309  //   return next(e)
310  // })
311
312  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
313    const { Box, Text, Markdown } = $.ui.resolve(e)
314    // The streamed text carries the plain prefix (for ctrl+o and exports);
315    // drawing swaps it for a coloured chip so the live view is not doubled.
316    const text = e.props.text
317    if (!text.startsWith(ANSWER_PREFIX)) return next(e)
318    const body = <Markdown text={'Hello2 ' + text.slice(ANSWER_PREFIX.length)} />
319    // The chip marks a turn that an extjs command-message started: only those
320    // draw it, every other prefixed message shows its content without the label.
321    if (hasCommandMessage && (isExtjsPrompt || isReExtPrompt)) {
322      return (
323        <Box flexDirection="column">
324          <Text color="yellow" bold>hello {ANSWER_PREFIX.trim()}</Text>
325          {body}
326        </Box>
327      )
328    }
329    return body
330  });
331
332  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
333    const { Box, Text } = $.ui.resolve(e)
334    const beneath = await next(e)
335    return (
336      <Box flexDirection="column">
337        <Box flexDirection="row">
338          <Text color="cyan" italic>Sencha Ext JS</Text>
339          <Text dimColor> · </Text>
340          <Text color="yellow" italic>Classic toolkit · Classic theme</Text>
341        </Box>
342        {beneath}
343      </Box>
344    )
345  });
346
347  on('ui.render', { component: 'ToolProgress' }, async ($, e, next) => {
348    if (e.requestId !== tool_use_id) return next(e)
349    await update($, messages, list => [...list, { title: 'ui.render:ToolProgress', message: JSON.stringify(e, null, spaces) }]);
350    const { Box, Text } = $.ui.resolve(e)
351    return Box({
352      flexDirection: 'row',
353      columnGap: 1,
354      children: [
355        Text({ color: 'cyan', children: ['⚡ Custom Progress:'] }),
356        // Call next(e) to retain engine defaults, or return your own custom elements
357        await next(e),
358      ]
359    });
360  })
361
362  on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
363    if (e.requestId !== tool_use_id) return next(e)
364    await update($, messages, list => [...list, { title: 'ui.render:ToolUse', message: JSON.stringify(e, null, spaces) }]);
365    const { Box, Text } = $.ui.resolve(e)
366    return Box({
367      flexDirection: 'row',
368      columnGap: 1,
369      children: [
370        Text({ color: 'cyan', children: ['⚡ Custom Tool Use:'] }),
371        // Call next(e) to retain engine defaults, or return your own custom elements
372        await next(e),
373      ]
374    });
375  })
376
377
378  // on('ui.render', { component: 'CommandOutput' }, async ($, e, next) => {
379  //   await update($, messages, list => [...list, { title: 'a', message: JSON.stringify(e, null, spaces) }]);
380  //   return next(e)
381  // })
382
383
384  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
385    const { Box, Text, Button } = $.ui.resolve(e)
386    const r: UpgradeRun = await read($, run)
387    const list: UpgradeStep[] = await read($, steps)
388    const feed: Message[] = await read($, messages)
389    const statuses: Message[] = await read($, status)
390    const facts: { k: string; v: string; color?: string }[] = []
391    if (r.projectType) {
392      facts.push({
393        k: 'Project',
394        v: [r.projectType, r.toolkit && `toolkit ${r.toolkit}`, r.theme].filter(Boolean).join(' · '),
395      })
396    }
397    if (r.sourceVersion) facts.push({ k: 'Version', v: `${r.sourceVersion}  →  v8` })
398    if (r.sourceRoot) facts.push({ k: 'Source', v: tail(r.sourceRoot) })
399    if (r.targetRoot) facts.push({ k: 'Target', v: tail(r.targetRoot) })
400    if (r.lintFolders !== undefined) {
401      const bad = (r.lintFoldersFailed ?? 0) > 0
402      facts.push({
403        k: 'Lint',
404        v: `${r.lintFolders} folders · ${r.lintFilesFixed ?? 0} fixed · ${r.lintFoldersFailed ?? 0} failed`,
405        color: bad ? 'error' : 'success',
406      })
407    }
408    facts.push({
409      k: 'Server',
410      v: r.serverUrl ?? 'stopped',
411      color: r.serverUrl ? 'success' : undefined,
412    })
413
414    let version =
415      <Text color="cyan" italic>{VERSION_STRING}</Text>
416
417    let buttons =
418      <Box flexDirection="row" marginTop={1}>
419        <Box borderStyle='single' marginLeft={0} paddingX={1} paddingY={0}>
420          <Button plain={true} key="clear" label="clear" onPress={async () => {
421            //$.ui.log(`clear: At ${new Date().toLocaleString()}`)
422            await update($, run, () => ({}))
423            await update($, steps, () => [])
424            await update($, messages, () => [])
425            await update($, status, () => [])
426          }}></Button>
427        </Box>
428        <Box borderStyle='single' marginLeft={2} paddingX={1} paddingY={0}>
429          <Button plain={true} key="close" label="close" onPress={() => { }} />
430        </Box>
431      </Box>;
432
433    let body =
434      <Box flexDirection="column">
435        <Box marginTop={2}><Text color="yellow" italic>FACTS</Text></Box>
436        {
437          facts.map(f => (
438            <Text key={`k-${f.k}`} color={f.color}>
439              {`${f.k.padEnd(8)}${f.v}`}
440            </Text>
441          ))
442        }
443
444        <Box marginTop={2}><Text color="yellow" italic>STATUS</Text></Box>
445        {
446          statuses.length === 0 ? (
447            <Text dimColor>No status yet.</Text>
448          ) : (
449            statuses.map((s, i) => (
450              <Text key={`step${i}`}>
451                <Text color="cyan">{s.title}</Text>
452                <Text dimColor> {s.message}</Text>
453              </Text>
454            ))
455          )
456        }
457
458        <Box marginTop={2}><Text color="yellow" italic>TOOL CALLS</Text></Box>
459        {
460          list.length === 0 ? (
461            <Text dimColor>No tool calls yet.</Text>
462          ) : (
463            list.map(s => (
464              <Box key={s.id} flexDirection="row" alignItems="flex-start">
465                <Box width={2} flexShrink={0}>
466                  <Text color={s.status === 'fail' ? 'error' : s.status === 'done' ? 'success' : 'warning'}>
467                    {s.status === 'runs' ? '•' : s.status === 'done' ? '✓' : '✗'}
468                  </Text>
469                </Box>
470                <Text color={s.status === 'fail' ? 'error' : s.status === 'done' ? 'success' : 'warning'}>
471                  {s.tool}
472                  {s.subject ? <Text dimColor>{` · ${tail(s.subject, 1280)}`}</Text> : null}
473                </Text>
474              </Box>
475            ))
476          )
477        }
478        <Box marginTop={2}><Text color="yellow" italic>STEPS</Text></Box>
479        {
480          feed.length === 0 ? (
481            <Text dimColor>No steps yet.</Text>
482          ) : (
483            feed.map((s, i) => (
484              <Text key={`step${i}`}>
485                <Text color="cyan">{s.title}</Text>
486                <Text dimColor> {s.message}</Text>
487              </Text>
488            ))
489          )
490        }
491      </Box>;
492
493    let bottom =
494      <Box flexDirection="row" marginTop={1}>
495        <Box borderStyle='single' marginLeft={0} paddingX={1} paddingY={0}>
496          <Button plain={true} key="clear" label="clear" onPress={async () => {
497            //$.ui.log(`clear: At ${new Date().toLocaleString()}`)
498            await update($, run, () => ({}))
499            await update($, steps, () => [])
500            await update($, messages, () => [])
501            await update($, status, () => [])
502          }}></Button>
503        </Box>
504        <Box borderStyle='single' marginLeft={2} paddingX={1} paddingY={0}>
505          <Button plain={true} key="close" label="close" onPress={() => { }} />
506        </Box>
507      </Box>;
508
509    if (e.surface !== 'terminal') {
510      const { Box, Text, Svg } = $.ui.resolve(e)
511      return (
512        <Box flexDirection="column">
513          <Box flexDirection="row">
514            <Svg source={await $.fs.read(`${$.plugin.root}/${LOGO_SVG}`)} width={65} height={25} alt="Sencha Ext JS" />
515            <Box marginLeft={2}>{version}</Box>
516            <Box marginLeft={2}><Text color="yellow" italic>{new Date().toLocaleString()}</Text></Box>
517          </Box>
518          {buttons}
519          {body}
520          {bottom}
521        </Box>
522      )
523    }
524
525    // terminal surface
526    const { Image } = $.ui.resolve(e)
527    return (
528      <Box flexDirection="column">
529        <Box flexDirection="row">
530          <Image source={{ png: (await $.fs.read(`${$.plugin.root}/${LOGO_PNG}`, { as: 'bytes' })).base64 }} columns={10} rows={1} alt="Sencha Ext JS" />
531          <Box marginLeft={2}>{version}</Box>
532          <Box marginLeft={2}><Text color="yellow" italic>{new Date().toLocaleString()}</Text></Box>
533        </Box>
534        {buttons}
535        {body}
536        {bottom}
537      </Box>
538    )
539  })
540}
541
types/index.d.ts 36 lines
1export type UpgradeRun = {
2  projectType?: string
3  toolkit?: string
4  theme?: string
5  sourceVersion?: string
6  sourceRoot?: string
7  targetRoot?: string
8  lintFolders?: number
9  lintFilesFixed?: number
10  lintFoldersFailed?: number
11  serverUrl?: string
12}
13
14export type UpgradeStep = {
15  id: string
16  tool: string
17  subject?: string
18  status: 'runs' | 'done' | 'fail'
19}
20
21export type Message = {
22  title: string
23  message: string
24}
25
26declare module 'claude-code' {
27  interface PluginState {
28    'sencha-extjs': { 
29      status: Message[]; 
30      run: UpgradeRun; 
31      steps: UpgradeStep[]; 
32      messages: Message[] 
33    }
34  }
35}
36