SLOPSHOPPER

awareness

A Claude Code pane showing the model, thinking level, context, usage limits, git state, PR, running agents and your last three prompts.

newpaneguardcommandpromptprocess
v0.1.4MITupdated 2026-10-08hatton/awareness
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · awareness
│ ┃ Awareness ✕ › fix the failing auth test and add an audit log call │ ┃ Awareness Mod │ ┃ ⏺ Read(src/auth.ts) │ ┃ Opus 5.5 | context: 49% ⎿ Read 6 lines │ ┃ usage: 5-hour 31% ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ Workspace: (detached) ⏺ Bash(bun test) │ ┃ file:///work/app ⎿ 3 pass, 1 fail │ ┃ 2 files uncommitted │ ┃ Branch not pushed (no upstream) ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ Recent Prompts ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ fix the failing auth test and add an › /awareness │ ┃ audit log call ⎿ awareness: Awareness pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Awareness
Awareness Mod Opus 5.5 | context: 49% usage: 5-hour 31% Workspace: (detached) file:///work/app 2 files uncommitted Branch not pushed (no upstream) Recent Prompts fix the failing auth test and add an audit log call
README

Awareness

A Claude Code mod that keeps a pane open beside the conversation, showing:

  • the model, its thinking level, and how full the context window is (red above 50%)
  • your 5-hour and weekly usage (red above 80%)
  • the workspace: branch, folder, the YouTrack card named by a BL- branch, and the branch's pull request with its status (draft, open, merged, closed)
  • uncommitted files and unpushed commits (orange when there are any)
  • subagents still running
  • your last three prompts, the newest highlighted

The pane docks beside the transcript in Claude Code's fullscreen layout when the terminal is at least 110 columns wide; otherwise it sits above the prompt. It opens itself when a session starts on a terminal at least 144 columns wide. Type /awareness to open it at any width.

The pull request comes from the GitHub CLI (gh), so gh must be installed and signed in for that part to show.

Install

In a Claude Code terminal session:

/plugin install awareness --marketplace hatton/awareness

Answer y to add the marketplace, then choose the user scope so it loads in every session.

Develop

claude --plugin-dir <path to this folder>
claude plugin validate <path to this folder>
claude plugin test <path to this folder>

License

MIT

Source 2 files
hooks/register.tsx 527 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Header, PullRequest, Recent, Repo, RunningAgent, WorkingOn } from '../types'
5
6const SHOWN = 3
7const PANE = 'awareness'
8const TITLE = 'Awareness'
9// Above this share of the context window, the figure turns red.
10const CONTEXT_WARNING_PERCENT = 50
11// Above this share of a usage window, the figure turns red.
12const LIMIT_WARNING_PERCENT = 80
13// How often git, agents and usage are read again, for changes made outside a turn.
14const REFRESH_MS = 10_000
15// The pull request is read over the network, so less often.
16const PULL_REQUEST_REFRESH_MS = 60_000
17const YOUTRACK_ISSUE_URL = 'https://issues.bloomlibrary.org/youtrack/issue/'
18
19// Oldest first, newest last; at most SHOWN entries.
20const recent = atom({ plugin: 'awareness', key: 'recent' } as const, [] as Recent)
21const header = atom({ plugin: 'awareness', key: 'header' } as const, {
22  model: null,
23  effort: null,
24  contextPercent: null,
25  limits: [],
26} as Header)
27const repo = atom({ plugin: 'awareness', key: 'repo' } as const, null as Repo)
28const pullRequest = atom({ plugin: 'awareness', key: 'pullRequest' } as const, null as PullRequest)
29const agents = atom({ plugin: 'awareness', key: 'agents' } as const, [] as readonly RunningAgent[])
30const NOTHING_YET: WorkingOn = { skill: null, tasks: {}, todo: null }
31const workingOn = atom({ plugin: 'awareness', key: 'workingOn' } as const, NOTHING_YET)
32
33/** Collapses a prompt's whitespace so blank lines in it don't stretch the pane. */
34const oneLine = (text: string) => text.replace(/\s+/g, ' ').trim()
35
36/**
37 * Turns a skill's name into a title: `preflight` reads `Preflight`,
38 * `youtrack-fix` `Youtrack fix`, and a plugin's `vercel:deploy` `Deploy`.
39 */
40export const skillTitle = (name: string) => {
41  const words = (name.split(':').pop() ?? name).replace(/[-_]+/g, ' ').trim()
42  return words.charAt(0).toUpperCase() + words.slice(1)
43}
44
45/**
46 * Lowers the first letter of a step phrase so it reads after a comma
47 * (`Fixing Devin bugs` reads `fixing Devin bugs`), but leaves a word in
48 * capitals alone (`CI`, `PR`).
49 */
50const lowerFirst = (phrase: string) =>
51  /^[A-Z][a-z]/.test(phrase) ? phrase.charAt(0).toLowerCase() + phrase.slice(1) : phrase
52
53/**
54 * Builds the "Working on:" line: the skill, then the step in progress; null
55 * when neither is known. The step is the in-progress task's phrase, else the
56 * in-progress item of the last `TodoWrite` list.
57 */
58export const workingOnText = (state: WorkingOn) => {
59  const task = Object.values(state.tasks).find(t => t.status === 'in_progress')
60  const step = task?.doing ?? state.todo
61  if (state.skill !== null && step !== null) {
62    return `Working on: ${state.skill}, ${lowerFirst(step)}`
63  }
64  if (state.skill !== null) {
65    return `Working on: ${state.skill}`
66  }
67  if (step !== null) {
68    return `Working on: ${step}`
69  }
70  return null
71}
72
73/**
74 * The skill a prompt starts, when it is a slash command: `/preflight thorough`
75 * names `preflight`, as does the `<command-name>/preflight</command-name>`
76 * form the prompt takes once Claude Code expands it.
77 */
78export const typedSkillName = (text: string) =>
79  /<command-name>\/?([\w:.-]+)<\/command-name>/.exec(text)?.[1] ?? /^\/([\w:.-]+)/.exec(text.trim())?.[1]
80
81/**
82 * Turns a model id such as `claude-opus-5-5[1m]` into `Opus 5.5 (1M context)`.
83 * Anything not spelled that way is shown as given.
84 */
85export const friendlyModelName = (model: string) => {
86  const match = /^claude-([a-z]+)-(\d+(?:-\d{1,2})*)(?:-\d{8})?(\[1m\])?$/i.exec(model)
87  if (!match) {
88    return model
89  }
90  const [, family = '', version = '', isLongContext] = match
91  const name = `${family.charAt(0).toUpperCase()}${family.slice(1)} ${version.replace(/-/g, '.')}`
92  return isLongContext ? `${name} (1M context)` : name
93}
94
95/** Names a usage window the way people say it. */
96const limitLabel = (kind: string) => {
97  switch (kind) {
98    case 'five_hour':
99      return '5-hour'
100    case 'seven_day':
101      return 'week'
102    case 'spend_limit':
103      return 'spend'
104    default:
105      return kind
106  }
107}
108
109/**
110 * Reads `git status --porcelain=v2 --branch` output into the repo's state.
111 * A detached HEAD shows as `(detached)`.
112 */
113export const parseGitStatus = (stdout: string, worktree: string): NonNullable<Repo> => {
114  let branch = '(detached)'
115  let unpushedCommits: number | null = null
116  let changedFiles = 0
117  for (const line of stdout.split(/\r?\n/)) {
118    if (line.startsWith('# branch.head ')) {
119      branch = line.slice('# branch.head '.length).trim()
120    } else if (line.startsWith('# branch.ab ')) {
121      const ahead = /\+(\d+)/.exec(line)
122      unpushedCommits = ahead ? Number(ahead[1]) : 0
123    } else if (line !== '' && !line.startsWith('#')) {
124      changedFiles += 1
125    }
126  }
127  return { branch, worktree, changedFiles, unpushedCommits }
128}
129
130/** Turns a folder path such as `D:\work` into a `file:///D:/work` URL that opens it. */
131export const folderUrl = (path: string) => `file:///${path.replace(/\\/g, '/').replace(/^\/+/, '')}`
132
133/** Describes the uncommitted files for the pane. */
134const uncommittedText = (count: number) => {
135  if (count === 0) {
136    return 'Nothing uncommitted'
137  }
138  return `${count} ${count === 1 ? 'file' : 'files'} uncommitted`
139}
140
141/** Describes the unpushed commits for the pane. */
142const unpushedText = (count: number | null) => {
143  if (count === null) {
144    return 'Branch not pushed (no upstream)'
145  }
146  if (count === 0) {
147    return 'Nothing unpushed'
148  }
149  return `${count} ${count === 1 ? 'commit' : 'commits'} not pushed`
150}
151
152/** Reads the model, the context window's fill and the usage windows into the header. */
153const refreshHeader = async ($: EngineInterface) => {
154  const [model, usage] = await Promise.all([$.session.model(), $.session.usage()])
155  await update($, header, h => ({
156    ...h,
157    model: friendlyModelName(model),
158    contextPercent: usage.context.percent ?? h.contextPercent,
159    limits: usage.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed })),
160  }))
161}
162
163/** Reads the thinking level from settings, for before the first request reports it. */
164const readEffortSetting = async ($: EngineInterface) => {
165  const { effortLevel } = await $.settings.read()
166  if (effortLevel === undefined) {
167    return
168  }
169  await update($, header, h => ({ ...h, effort: String(effortLevel) }))
170}
171
172/** Reads the worktree's branch, uncommitted files and unpushed commits. */
173const refreshRepo = async ($: EngineInterface) => {
174  const worktree = await $.session.cwd()
175  const { exitCode, stdout } = await $.process.run(['git', 'status', '--porcelain=v2', '--branch'], {
176    timeoutMs: 10_000,
177  })
178  const state: Repo = exitCode === 0 ? parseGitStatus(stdout, worktree) : null
179  await update($, repo, () => state)
180}
181
182/**
183 * Turns a GitHub pull request URL such as
184 * `https://github.com/BloomBooks/BloomDesktop/pull/8315` into its Reviewable
185 * page, `https://reviewable.io/reviews/BloomBooks/BloomDesktop/8315`.
186 */
187export const reviewableUrl = (pullRequestUrl: string) =>
188  pullRequestUrl.replace(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+).*$/, 'https://reviewable.io/reviews/$1/$2/$3')
189
190/**
191 * Reads `gh pr view --json number,url,state,isDraft` output into the pull
192 * request's state: a draft reads `draft`, otherwise the state in lower case.
193 */
194export const parsePullRequest = (stdout: string): NonNullable<PullRequest> => {
195  const pr = JSON.parse(stdout) as { number: number; url: string; state: string; isDraft: boolean }
196  const status = pr.isDraft && pr.state === 'OPEN' ? 'draft' : pr.state.toLowerCase()
197  return { number: pr.number, url: pr.url, status }
198}
199
200/** Reads the current branch's pull request through the GitHub CLI. */
201const refreshPullRequest = async ($: EngineInterface) => {
202  const { exitCode, stdout } = await $.process.run(
203    ['gh', 'pr', 'view', '--json', 'number,url,state,isDraft'],
204    { timeoutMs: 20_000 },
205  )
206  const state: PullRequest = exitCode === 0 ? parsePullRequest(stdout) : null
207  await update($, pullRequest, () => state)
208}
209
210/** Reads which subagents and teammates are still at work. */
211const refreshAgents = async ($: EngineInterface) => {
212  const list = await $.agent.list()
213  const running = list
214    .filter(a => a.status === 'running' || a.status === 'pending' || a.status === 'waiting')
215    .map(a => ({ id: a.id, description: a.description, type: a.type }))
216  await update($, agents, () => running)
217}
218
219/**
220 * Applies a `TaskUpdate` to the tracked task list: a new status or phrase, or
221 * the task's removal when it is deleted. An id the list never saw (one
222 * created before the list was last cleared) is added only when the update
223 * names what it does; otherwise the list is left as it was.
224 */
225export const updatedTasks = (
226  tasks: WorkingOn['tasks'],
227  change: { taskId: string; status?: string; activeForm?: string; subject?: string },
228): WorkingOn['tasks'] => {
229  if (change.status === 'deleted') {
230    const { [change.taskId]: _removed, ...rest } = tasks
231    return rest
232  }
233  const known = tasks[change.taskId]
234  const named = change.activeForm ?? change.subject
235  if (known === undefined && named === undefined) {
236    return tasks
237  }
238  const before = known ?? { doing: named ?? '', status: 'pending' as const }
239  const status =
240    change.status === 'in_progress' || change.status === 'completed' || change.status === 'pending'
241      ? change.status
242      : before.status
243  return {
244    ...tasks,
245    [change.taskId]: { doing: change.activeForm ?? change.subject ?? before.doing, status },
246  }
247}
248
249/** Takes the activity from a prompt that runs a skill as a slash command. */
250const noteTypedSkill = async ($: EngineInterface, text: string) => {
251  const name = typedSkillName(text)
252  if (name === undefined) {
253    return
254  }
255  const command = (await $.command.list()).find(c => c.name === name)
256  // Built-in commands (/clear, /model) and this mod's own name no activity.
257  if (command === undefined || command.source === 'builtin' || command.plugin === 'awareness') {
258    return
259  }
260  await update($, workingOn, w => ({ ...w, skill: skillTitle(name) }))
261}
262
263/** Reads everything the pane shows that can change outside a turn. */
264const refreshAll = async ($: EngineInterface) => {
265  await Promise.allSettled([refreshHeader($), refreshRepo($), refreshAgents($)])
266}
267
268export const register: Register = on => {
269  // Claude Code docks the pane beside the transcript in the fullscreen layout
270  // from 110 columns; elsewhere it seats it above the prompt.
271  on('session.start', async ($, e, next) => {
272    await $.command.register({
273      name: 'awareness',
274      description: 'Show the Awareness pane: model, git state, usage, agents and your last three prompts',
275    })
276    void $.ui.open({ id: PANE, title: TITLE })
277    await Promise.allSettled([refreshAll($), readEffortSetting($)])
278    void refreshPullRequest($).catch(() => undefined)
279    $.clock.every(REFRESH_MS, () => refreshAll($))
280    $.clock.every(PULL_REQUEST_REFRESH_MS, () => refreshPullRequest($).catch(() => undefined))
281
282    return next(e)
283  })
284
285  // /clear starts the conversation over, so the activity starts over too.
286  on('classic.SessionStart', async ($, e, next) => {
287    if (e.source === 'clear') {
288      await update($, workingOn, () => NOTHING_YET).catch(() => undefined)
289    }
290    return next(e)
291  })
292
293  // A slash command typed at the prompt; noteTypedSkill skips built-ins.
294  on('command.run', async ($, e, next) => {
295    if (e.command !== 'awareness') {
296      await noteTypedSkill($, `/${e.command}`).catch(() => undefined)
297    }
298    return next(e)
299  })
300
301  on('command.run', { command: 'awareness' }, async $ => {
302    await $.ui.open({ id: PANE, title: TITLE })
303
304    return { text: 'Awareness pane opened.' }
305  })
306
307  // Records only what the person typed (here or through Remote Control), not
308  // prompts raised by plugins, schedules, task notifications or other agents.
309  on('prompt.submit', async ($, e, next) => {
310    const text = oneLine(e.text)
311    if ((e.origin.kind === 'composer' || e.origin.kind === 'bridge') && text) {
312      // A failed write must not hold up the prompt.
313      await update($, recent, list => [...list, text].slice(-SHOWN)).catch(() => undefined)
314      await noteTypedSkill($, e.text).catch(() => undefined)
315    }
316    return next(e)
317  })
318
319  // The model starting a skill names the activity.
320  on('tool.call', { tool: 'Skill' }, async ($, e, next) => {
321    const result = await next(e)
322    if (e.agentId === undefined && result.deny === undefined && result.isError !== true) {
323      await update($, workingOn, w => ({ ...w, skill: skillTitle(e.skill) })).catch(() => undefined)
324    }
325    return result
326  })
327
328  // The main conversation's task list names the step in progress.
329  on('tool.call', { tool: 'TaskCreate' }, async ($, e, next) => {
330    const result = await next(e)
331    if (e.agentId === undefined && result.deny === undefined && result.isError !== true) {
332      const doing = e.activeForm ?? e.subject
333      await update($, workingOn, w => ({
334        ...w,
335        tasks: { ...w.tasks, [result.result.task.id]: { doing, status: 'pending' as const } },
336      })).catch(() => undefined)
337    }
338    return result
339  })
340
341  on('tool.call', { tool: 'TaskUpdate' }, async ($, e, next) => {
342    const result = await next(e)
343    if (e.agentId === undefined && result.deny === undefined && result.isError !== true) {
344      await update($, workingOn, w => ({ ...w, tasks: updatedTasks(w.tasks, e) })).catch(() => undefined)
345    }
346    return result
347  })
348
349  on('tool.call', { tool: 'TodoWrite' }, async ($, e, next) => {
350    const result = await next(e)
351    if (e.agentId === undefined && result.deny === undefined && result.isError !== true) {
352      const todo = e.todos.find(t => t.status === 'in_progress')?.activeForm ?? null
353      await update($, workingOn, w => ({ ...w, todo })).catch(() => undefined)
354    }
355    return result
356  })
357
358  // Each main-loop request carries the effort it asks for, and its response
359  // moves the context window's fill.
360  on('turn.step', async function* ($, e, next) {
361    if (e.agentId !== undefined) {
362      return yield* next(e)
363    }
364    const effort = e.effort === undefined ? null : String(e.effort)
365    await update($, header, h => ({ ...h, effort })).catch(() => undefined)
366    const response = yield* next(e)
367    await refreshHeader($).catch(() => undefined)
368
369    return response
370  })
371
372  // A tool call can commit, push, edit a file or start an agent.
373  on('tool.call', async ($, e, next) => {
374    const result = await next(e)
375    void Promise.allSettled([refreshRepo($), refreshAgents($)])
376    return result
377  })
378
379  // The main conversation's turn ending leaves Claude idle, so nothing is being worked on.
380  on('turn.complete', async ($, e, next) => {
381    const result = await next(e)
382    if (e.agentId === undefined) {
383      await update($, workingOn, () => NOTHING_YET).catch(() => undefined)
384    }
385    void refreshAll($)
386    void refreshPullRequest($).catch(() => undefined)
387    return result
388  })
389
390  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
391    const { Box, Text, Link } = $.ui.resolve(e)
392    const list = await read($, recent)
393    const top = await read($, header)
394    const git = await read($, repo)
395    const pr = await read($, pullRequest)
396    const working = await read($, agents)
397    const activity = workingOnText(await read($, workingOn))
398    const newest = list.length - 1
399    const percent = top.contextPercent
400    const card = git === null ? undefined : /^BL-\d+/i.exec(git.branch)?.[0].toUpperCase()
401
402    return (
403      <Box flexDirection="column" gap={1} paddingLeft={2}>
404        <Text bold dimColor>
405          Awareness Mod
406        </Text>
407        <Box flexDirection="column">
408          <Text wrap="truncate-end">
409            <Text dimColor>
410              {top.model ?? '…'}
411              {top.effort === null ? '' : ` (${top.effort})`} | context:{' '}
412            </Text>
413            <Text
414              color={percent !== null && percent > CONTEXT_WARNING_PERCENT ? 'error' : undefined}
415              dimColor={percent === null || percent <= CONTEXT_WARNING_PERCENT}
416            >
417              {percent === null ? '…' : `${percent}%`}
418            </Text>
419          </Text>
420          {top.limits.length > 0 && (
421            <Text wrap="truncate-end">
422              <Text dimColor>usage: </Text>
423              {top.limits.map((l, i) => (
424                <Text
425                  key={l.kind}
426                  color={l.percentUsed > LIMIT_WARNING_PERCENT ? 'error' : undefined}
427                  dimColor={l.percentUsed <= LIMIT_WARNING_PERCENT}
428                >
429                  {i > 0 ? ' · ' : ''}
430                  {limitLabel(l.kind)} {Math.round(l.percentUsed)}%
431                </Text>
432              ))}
433            </Text>
434          )}
435        </Box>
436
437        {git !== null && (
438          <Box flexDirection="column">
439            <Text bold dimColor wrap="truncate-end">
440              Workspace: {git.branch}
441            </Text>
442            <Link href={folderUrl(git.worktree)}>
443              <Text dimColor underline wrap="truncate-middle">
444                {git.worktree}
445              </Text>
446            </Link>
447            {(card !== undefined || pr !== null) && (
448              <Box flexDirection="row" gap={2}>
449                {card !== undefined && (
450                  <Link href={`${YOUTRACK_ISSUE_URL}${card}`}>
451                    <Text dimColor underline>
452                      {card}
453                    </Text>
454                  </Link>
455                )}
456                {pr !== null && (
457                  <Box flexDirection="row" gap={1}>
458                    <Link href={pr.url}>
459                      <Text dimColor underline>
460                        PR #{pr.number}
461                      </Text>
462                    </Link>
463                    <Link href={reviewableUrl(pr.url)}>
464                      <Text dimColor underline>
465                        Reviewable
466                      </Text>
467                    </Link>
468                    <Text dimColor>({pr.status})</Text>
469                  </Box>
470                )}
471              </Box>
472            )}
473            <Text color={git.changedFiles > 0 ? 'warning' : undefined} dimColor={git.changedFiles === 0}>
474              {uncommittedText(git.changedFiles)}
475            </Text>
476            <Text
477              color={git.unpushedCommits !== 0 ? 'warning' : undefined}
478              dimColor={git.unpushedCommits === 0}
479            >
480              {unpushedText(git.unpushedCommits)}
481            </Text>
482          </Box>
483        )}
484
485        {working.length > 0 && (
486          <Box flexDirection="column">
487            <Text bold dimColor>
488              Running Agents
489            </Text>
490            {working.map(a => (
491              <Box key={a.id} paddingLeft={2}>
492                <Text dimColor wrap="truncate-end">
493                  {a.description} ({a.type})
494                </Text>
495              </Box>
496            ))}
497          </Box>
498        )}
499
500        {activity !== null && (
501          <Text color="suggestion" wrap="truncate-end">
502            {activity}
503          </Text>
504        )}
505
506        <Box flexDirection="column" gap={1}>
507          <Text bold dimColor>
508            Recent Prompts
509          </Text>
510          {list.length === 0 && <Text dimColor>No prompts yet.</Text>}
511          {list.map((text, i) => (
512            <Box key={`p${i}`} paddingLeft={2}>
513              {i === newest ? (
514                <Text bold color="suggestion">
515                  {text}
516                </Text>
517              ) : (
518                <Text dimColor>{text}</Text>
519              )}
520            </Box>
521          ))}
522        </Box>
523      </Box>
524    )
525  })
526}
527
types/index.d.ts 74 lines
1/** The person's most recent prompts, oldest first. */
2export type Recent = readonly string[]
3
4/** One subscription usage window, as a share used. */
5export type Limit = {
6  /** `five_hour`, `seven_day`, or a gateway's `spend_limit`. */
7  kind: string
8  percentUsed: number
9}
10
11/** What the pane's top lines show; each part null until it is known. */
12export type Header = {
13  /** The main loop's model, as `/model` shows it. */
14  model: string | null
15  /** The effort the main loop's last request asked for. */
16  effort: string | null
17  /** How full the context window is, as a whole percentage. */
18  contextPercent: number | null
19  /** The subscription usage windows; empty off a subscription. */
20  limits: readonly Limit[]
21}
22
23/** The git state of the session's worktree; null outside a repository. */
24export type Repo = {
25  branch: string
26  /** The session's directory. */
27  worktree: string
28  /** Files with changes not yet committed, untracked ones included. */
29  changedFiles: number
30  /** Commits on the branch not on its upstream; null when it has no upstream. */
31  unpushedCommits: number | null
32} | null
33
34/** The branch's pull request; null when it has none or `gh` cannot say. */
35export type PullRequest = {
36  number: number
37  url: string
38  /** `draft`, `open`, `merged` or `closed`. */
39  status: string
40} | null
41
42/** One entry of the main conversation's task list, by its id. */
43export type TrackedTask = {
44  /** The present-continuous phrase shown beside the spinner, else the subject. */
45  doing: string
46  status: 'pending' | 'in_progress' | 'completed'
47}
48
49/** What the "Working on:" line is made from. */
50export type WorkingOn = {
51  /** The skill last started, as the person or the model named it. */
52  skill: string | null
53  /** The `TaskCreate`/`TaskUpdate` task list, by task id. */
54  tasks: Readonly<Record<string, TrackedTask>>
55  /** The in-progress item of the last `TodoWrite` list, if any. */
56  todo: string | null
57}
58
59/** A subagent or teammate still at work. */
60export type RunningAgent = { id: string; description: string; type: string }
61
62declare module 'claude-code' {
63  interface PluginState {
64    awareness: {
65      recent: Recent
66      header: Header
67      repo: Repo
68      pullRequest: PullRequest
69      workingOn: WorkingOn
70      agents: readonly RunningAgent[]
71    }
72  }
73}
74