@thegitai/cli 1.0.0-preview.34 → 1.0.0-preview.35

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.
@@ -7,6 +7,7 @@ import { isUserInputQuestionArray } from './contracts.js';
7
7
  import { createTraceContext, gatewayFailureCategory, normalizeServerUrl, readErrorResponse, } from './http.js';
8
8
  import { collectClientEnvironment } from '../client-environment.js';
9
9
  import { collectProjectOrientation } from '../project-orientation.js';
10
+ import { describeSessionImageStore } from '../core/session-image-store.js';
10
11
  import { autoAttachImages } from '../core/image-path-extractor.js';
11
12
  import { formatTurnFailureMarker } from '../turn-failure-marker.js';
12
13
  export class TurnCancelledError extends Error {
@@ -546,6 +547,7 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
546
547
  backgroundJobUpdate: backgroundJobUpdate || undefined,
547
548
  clientEnvironment: collectClientEnvironment({ env: session.env }),
548
549
  projectOrientation: collectProjectOrientation(session.rootDir) ?? undefined,
550
+ sessionImageStore: describeSessionImageStore(session.env) ?? undefined,
549
551
  imageAttachments: imageAttachmentsForServer(requestImageAttachments),
550
552
  maxToolSteps: session.maxToolSteps,
551
553
  autoYes: session.autoYes,
@@ -111,6 +111,53 @@ export function readSessionImageByIndex(index, env = process.env) {
111
111
  }
112
112
  return null;
113
113
  }
114
+ export function describeSessionImageStore(env = process.env) {
115
+ if (!activeSessionId)
116
+ return null;
117
+ const dir = getSessionImageDir(activeSessionId, env);
118
+ if (!existsSync(dir))
119
+ return null;
120
+ const indices = [];
121
+ try {
122
+ for (const name of readdirSync(dir)) {
123
+ const parsed = Number.parseInt(path.basename(name, path.extname(name)), 10);
124
+ if (Number.isInteger(parsed) && parsed >= 1)
125
+ indices.push(parsed);
126
+ }
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ if (indices.length === 0)
132
+ return null;
133
+ return { dir, indices: indices.sort((a, b) => a - b) };
134
+ }
135
+ const MAX_STORE_DIR_CHARS = 512;
136
+ const MAX_STORE_INDICES = 200;
137
+ const STORE_CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
138
+ export function normalizeSessionImageStore(value) {
139
+ if (!value || typeof value !== 'object')
140
+ return null;
141
+ const record = value;
142
+ const dir = String(record.dir ?? '')
143
+ .replace(STORE_CONTROL_CHARS, ' ')
144
+ .trim()
145
+ .slice(0, MAX_STORE_DIR_CHARS);
146
+ if (!dir)
147
+ return null;
148
+ const seen = new Set();
149
+ for (const raw of Array.isArray(record.indices) ? record.indices : []) {
150
+ const parsed = Number(raw);
151
+ if (!Number.isInteger(parsed) || parsed < 1)
152
+ continue;
153
+ seen.add(parsed);
154
+ if (seen.size >= MAX_STORE_INDICES)
155
+ break;
156
+ }
157
+ if (seen.size === 0)
158
+ return null;
159
+ return { dir, indices: [...seen].sort((a, b) => a - b) };
160
+ }
114
161
  export class SessionImageError extends Error {
115
162
  code;
116
163
  constructor(message, code) {
@@ -148,6 +148,17 @@ export const SLASH_COMMANDS = [
148
148
  description: 'Quit the current session',
149
149
  },
150
150
  ];
151
+ export function composerAttachmentsAfterCancel(queuedAttachments, inFlightAttachments) {
152
+ const seen = new Set();
153
+ const kept = [];
154
+ for (const attachment of [...queuedAttachments, ...inFlightAttachments]) {
155
+ if (seen.has(attachment.index))
156
+ continue;
157
+ seen.add(attachment.index);
158
+ kept.push(attachment);
159
+ }
160
+ return kept;
161
+ }
151
162
  function getShellWidth(columns) {
152
163
  const safeColumns = Math.max(columns, 20);
153
164
  const targetWidth = Math.floor(safeColumns * TUI_WIDTH_RATIO);
@@ -1414,6 +1425,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1414
1425
  let latestUsageSummary = null;
1415
1426
  let pendingTurnEntries = [];
1416
1427
  let activeTurnAbort = null;
1428
+ let activeTurnImageAttachments = [];
1417
1429
  let activeServerTurnId = null;
1418
1430
  let unacknowledged = new Map();
1419
1431
  const blockedRows = new WeakMap();
@@ -1675,6 +1687,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1675
1687
  activeTurnAbort = null;
1676
1688
  cancelActiveCommand();
1677
1689
  const queued = store.getState().queuedMessage;
1690
+ const restoredAttachments = composerAttachmentsAfterCancel(queued ? queued.imageAttachments : [], activeTurnImageAttachments);
1691
+ activeTurnImageAttachments = [];
1678
1692
  const cancelledEntries = [
1679
1693
  ...takePendingTurnEntries(),
1680
1694
  {
@@ -1692,7 +1706,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1692
1706
  busySince: null,
1693
1707
  commandLog: [],
1694
1708
  cursor: queued ? queued.body.length : current.cursor,
1695
- imageAttachments: queued ? queued.imageAttachments : [],
1709
+ imageAttachments: restoredAttachments,
1696
1710
  input: queued ? queued.body : current.input,
1697
1711
  pastedChunks: queued ? queued.pastedChunks : current.pastedChunks,
1698
1712
  promptHistoryCursor: queued ? null : current.promptHistoryCursor,
@@ -2673,6 +2687,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2673
2687
  pendingTurnEntries = [];
2674
2688
  queueTurnEntry(userEntry);
2675
2689
  resetThinkingPacer();
2690
+ activeTurnImageAttachments = imageAttachments;
2676
2691
  store.update((current) => ({
2677
2692
  ...current,
2678
2693
  activeTurnInput: input,
@@ -2726,6 +2741,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2726
2741
  if (!(await saveActiveSession()))
2727
2742
  return;
2728
2743
  syncShellStateFromSession();
2744
+ activeTurnImageAttachments = [];
2729
2745
  store.update((current) => ({
2730
2746
  ...current,
2731
2747
  activeTurnInput: '',
@@ -2779,6 +2795,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2779
2795
  const cancelled = isTurnCancelledError(error);
2780
2796
  if (exitForAuthenticationError(error))
2781
2797
  return;
2798
+ const failedTurnAttachments = activeTurnImageAttachments;
2799
+ activeTurnImageAttachments = [];
2782
2800
  store.update((current) => ({
2783
2801
  ...current,
2784
2802
  activeTurnInput: '',
@@ -2787,7 +2805,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2787
2805
  busyPausedAt: null,
2788
2806
  busySince: null,
2789
2807
  commandLog: [],
2790
- imageAttachments: [],
2808
+ imageAttachments: failedTurnAttachments,
2791
2809
  status: cancelled ? 'Ready' : 'Turn failed',
2792
2810
  thinkingTitle: '',
2793
2811
  thinkingNotes: [],
@@ -41,6 +41,48 @@ const MODEL_PICKER_BORDER_COLOR = 'cyan';
41
41
  const MODEL_PICKER_ACCENT_COLOR = 'cyan';
42
42
  const MODEL_PICKER_HIGHLIGHT_BG = 'ansi256(87)';
43
43
  const MODEL_PICKER_META_INDENT = ' ';
44
+ const COMPOSER_MARKER_PATTERN = /\[Image #\d+\]|\[Pasted Text: [^\]\n]*\]/g;
45
+ function composerMarkerMask(text) {
46
+ const mask = new Array(text.length).fill(false);
47
+ for (const match of text.matchAll(COMPOSER_MARKER_PATTERN)) {
48
+ const start = match.index ?? 0;
49
+ for (let offset = start; offset < start + match[0].length; offset += 1) {
50
+ mask[offset] = true;
51
+ }
52
+ }
53
+ return mask;
54
+ }
55
+ function composerRowSpans(text, mask, cursorCol) {
56
+ const spans = [];
57
+ const push = (value, marker, cursor) => {
58
+ if (!value)
59
+ return;
60
+ spans.push(span(value, {
61
+ ...(marker ? { color: 'cyan' } : {}),
62
+ ...(cursor ? { inverse: true } : {}),
63
+ }));
64
+ };
65
+ let cut = 0;
66
+ const flushTo = (end) => {
67
+ while (cut < end) {
68
+ const marker = mask[cut] ?? false;
69
+ let runEnd = cut + 1;
70
+ while (runEnd < end && (mask[runEnd] ?? false) === marker)
71
+ runEnd += 1;
72
+ push(text.slice(cut, runEnd), marker, false);
73
+ cut = runEnd;
74
+ }
75
+ };
76
+ if (cursorCol === null) {
77
+ flushTo(text.length);
78
+ return spans;
79
+ }
80
+ flushTo(Math.min(cursorCol, text.length));
81
+ push(text[cursorCol] ?? ' ', mask[cursorCol] ?? false, true);
82
+ cut = Math.min(cursorCol + 1, text.length);
83
+ flushTo(text.length);
84
+ return spans;
85
+ }
44
86
  function buildComposerInputLines(input, cursor, promptLabel, placeholder, width) {
45
87
  if (!input) {
46
88
  return [
@@ -51,20 +93,22 @@ function buildComposerInputLines(input, cursor, promptLabel, placeholder, width)
51
93
  const inputWidth = Math.max(1, width - labelWidth);
52
94
  const { cursorCol, cursorRow, rows } = splitComposerInput(input, cursor, inputWidth);
53
95
  const firstRow = Math.min(Math.max(cursorRow - COMPOSER_INPUT_MAX_ROWS + 1, 0), Math.max(rows.length - COMPOSER_INPUT_MAX_ROWS, 0));
96
+ const rowOffsets = [];
97
+ let consumed = 0;
98
+ for (const row of rows) {
99
+ rowOffsets.push(consumed);
100
+ consumed += row.length;
101
+ }
102
+ const mask = composerMarkerMask(rows.join(''));
54
103
  return rows
55
104
  .slice(firstRow, firstRow + COMPOSER_INPUT_MAX_ROWS)
56
105
  .map((text, index) => {
57
106
  const absoluteRow = firstRow + index;
107
+ const rowStart = rowOffsets[absoluteRow] ?? 0;
58
108
  const label = index === 0
59
109
  ? span(promptLabel, { color: 'cyan' })
60
110
  : span(' '.repeat(labelWidth));
61
- if (absoluteRow !== cursorRow) {
62
- return line(label, span(text));
63
- }
64
- const before = text.slice(0, cursorCol);
65
- const cursorChar = text[cursorCol] ?? ' ';
66
- const after = text.slice(cursorCol + 1);
67
- return line(label, span(before), span(cursorChar, { inverse: true }), span(after));
111
+ return line(label, ...composerRowSpans(text, mask.slice(rowStart, rowStart + text.length), absoluteRow === cursorRow ? cursorCol : null));
68
112
  });
69
113
  }
70
114
  function getEntryColor(kind) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegitai/cli",
3
- "version": "1.0.0-preview.34",
3
+ "version": "1.0.0-preview.35",
4
4
  "description": "TheGitAI is an agentic AI coding tool for your terminal. It reads and searches your repository, writes and edits files, runs your tests, and verifies the change before handing it back.",
5
5
  "keywords": [
6
6
  "agentic-ai",
@@ -44,11 +44,11 @@
44
44
  "@lydell/node-pty-linux-x64": "1.1.0",
45
45
  "@lydell/node-pty-win32-arm64": "1.1.0",
46
46
  "@lydell/node-pty-win32-x64": "1.1.0",
47
- "@thegitai/tui-darwin-arm64": "1.0.0-preview.34",
48
- "@thegitai/tui-darwin-x64": "1.0.0-preview.34",
49
- "@thegitai/tui-linux-arm64": "1.0.0-preview.34",
50
- "@thegitai/tui-linux-x64": "1.0.0-preview.34",
51
- "@thegitai/tui-win32-x64": "1.0.0-preview.34",
47
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.35",
48
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.35",
49
+ "@thegitai/tui-linux-arm64": "1.0.0-preview.35",
50
+ "@thegitai/tui-linux-x64": "1.0.0-preview.35",
51
+ "@thegitai/tui-win32-x64": "1.0.0-preview.35",
52
52
  "@vscode/ripgrep": "1.18.0"
53
53
  },
54
54
  "repository": {