Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/btw-readonly-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": minor
---

Side questions started with /btw can call the read-only tools Read, Grep and Glob.
5 changes: 5 additions & 0 deletions .changeset/delete-session-from-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": minor
---

Delete a session from the session picker with Ctrl+X.
5 changes: 5 additions & 0 deletions .changeset/glob-pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": minor
---

Glob accepts `offset` and `head_limit` to page through matching paths, and `head_limit: 0` returns every match.
5 changes: 5 additions & 0 deletions .changeset/mcp-structured-results.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Keep an MCP tool's structured result alongside its text and media output.
5 changes: 5 additions & 0 deletions .changeset/refresh-config-warnings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Refresh configuration warnings after a settings change instead of keeping them until the next restart.
5 changes: 5 additions & 0 deletions .changeset/subagent-model-disclosure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

The subagent model list now names the model `primary` is bound to and states that pool entries do not inherit your thinking level.
5 changes: 5 additions & 0 deletions .changeset/subagent-timeout-zero-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Accept `0` from the subagent timeout environment variable to disable the timeout, matching the config file.
5 changes: 5 additions & 0 deletions .changeset/tasks-list-agent-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Show each background agent's model in the /tasks list.
5 changes: 5 additions & 0 deletions .changeset/toml-parser-security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Update the TOML parser to a version that is not affected by a denial-of-service advisory.
5 changes: 5 additions & 0 deletions .changeset/tower-build-mission-tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Require at least one task on a tower build mission; read-only survey missions still need none.
5 changes: 5 additions & 0 deletions .changeset/tower-mission-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": minor
---

Tower missions take a `context` field that carries your own words verbatim to the worker and the reviewer, and tower spawns honour the configured subagent timeout.
5 changes: 5 additions & 0 deletions .changeset/warn-malformed-models-entry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Warn when a `[models]` entry has no `model` field, including when an unquoted dotted alias parsed as a nested table.
2 changes: 1 addition & 1 deletion apps/pythinker-code/dist-web/.web-bundle-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"sourceHash": "be9d504284ff67b737fb3dbf93f28e4c6d3c8bff41f281a07505660bcdfa77f0",
"sourceHash": "3c20602f9f36046bbb45ad066b2c91e149cc3f8c7f06dc5430c73e7f29537161",
"sourceFileCount": 493
}
2 changes: 1 addition & 1 deletion apps/pythinker-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"postject": "1.0.0-alpha.6",
"qrcode": "^1.5.4",
"semver": "^7.7.4",
"smol-toml": "^1.6.1",
"smol-toml": "^1.7.1",
"tsx": "^4.23.5",
"ws": "^8.21.3",
"yazl": "^3.3.1",
Expand Down
3 changes: 3 additions & 0 deletions apps/pythinker-code/src/cli/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createPythinkerDeviceId } from '@pymodel/pythinker-code-oauth';
import {
loadRuntimeConfigSafe,
log,
resolveConfigPath,
resolvePythinkerHome,
type PythinkerConfig,
Expand Down Expand Up @@ -56,6 +57,7 @@ export function initializeCliTelemetry(options: InitializeCliTelemetryOptions):
model: options.model ?? options.config.defaultModel,
sessionId: options.sessionId,
endpoint: () => currentPythinkerProfile().telemetryEndpoint,
onUnexpectedError: (error) => log.warn('telemetry property dropped', { error: String(error) }),
});
if (options.bootstrap.firstLaunch) {
options.harness.track('first_launch');
Expand Down Expand Up @@ -98,6 +100,7 @@ export function initializeServerTelemetry(
uiMode: WEB_UI_MODE,
model: config.defaultModel,
endpoint: () => currentPythinkerProfile().telemetryEndpoint,
onUnexpectedError: (error) => log.warn('telemetry property dropped', { error: String(error) }),
});

return {
Expand Down
89 changes: 86 additions & 3 deletions apps/pythinker-code/src/tui/components/dialogs/session-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@pymodel/pi-tui';
import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols';
import { currentTheme } from '#/tui/theme';
import { printableChar } from '#/tui/utils/printable-key';
import { SearchableList } from '#/tui/utils/searchable-list';

export interface SessionRow {
Expand Down Expand Up @@ -80,7 +81,7 @@ function sessionSearchText(session: SessionRow): string {
export class SessionPickerComponent extends Container implements Focusable {
private sessions: SessionRow[];
private currentSessionId: string;
private onSelect: (session: SessionRow) => void;
private onSelect: (session: SessionRow) => void | Promise<void>;
private onCancel: () => void;
private onToggleScope?: (selectedSessionId: string) => void;
private maxVisibleSessions: number;
Expand All @@ -91,6 +92,8 @@ export class SessionPickerComponent extends Container implements Focusable {
private hasMore: boolean;
private loadingMore: boolean;
private list: SearchableList<SessionRow>;
private deleteState?: { session: SessionRow; phase: 'confirm' | 'deleting' };
private selectInFlight = false;

focused = false;

Expand All @@ -101,7 +104,7 @@ export class SessionPickerComponent extends Container implements Focusable {
scope?: 'cwd' | 'all';
initialSelectedSessionId?: string;
pageSize?: number;
onSelect: (session: SessionRow) => void;
onSelect: (session: SessionRow) => void | Promise<void>;
onCancel: () => void;
onCtrlC?: () => void;
onCtrlD?: () => void;
Expand All @@ -115,6 +118,8 @@ export class SessionPickerComponent extends Container implements Focusable {
onLoadMore?: () => void;
/** Fired when a search query becomes active while pages remain unfetched. */
onSearchDrain?: () => void;
/** Fired after the user confirms deletion with `y`; the picker clears its delete state once the request settles. */
onDeleteRequest?: (session: SessionRow) => Promise<void>;
}) {
super();
this.sessions = opts.sessions;
Expand Down Expand Up @@ -142,12 +147,14 @@ export class SessionPickerComponent extends Container implements Focusable {
this.visibleCount = Math.min(this.sessions.length, initialLoadedPages * this.pageSize);
this.onCtrlC = opts.onCtrlC;
this.onCtrlD = opts.onCtrlD;
this.onDeleteRequest = opts.onDeleteRequest;
}

private readonly onCtrlC?: () => void;
private readonly onCtrlD?: () => void;
private readonly onLoadMore?: () => void;
private readonly onSearchDrain?: () => void;
private readonly onDeleteRequest?: (session: SessionRow) => Promise<void>;

/** Appends a freshly fetched page, keeping the cursor and active query. */
appendSessions(rows: SessionRow[]): void {
Expand Down Expand Up @@ -209,6 +216,13 @@ export class SessionPickerComponent extends Container implements Focusable {
}

handleInput(data: string): void {
if (this.deleteState !== undefined) {
this.handleDeleteInput(data);
return;
}
// A selection runs resume/switch asynchronously; input during that window
// (e.g. Ctrl+X delete) would race the session swap.
if (this.selectInFlight) return;
if (matchesKey(data, Key.ctrl('c'))) {
this.onCtrlC?.();
return;
Expand All @@ -221,6 +235,14 @@ export class SessionPickerComponent extends Container implements Focusable {
this.onToggleScope?.(this.list.selected()?.id ?? this.currentSessionId);
return;
}
if (matchesKey(data, Key.ctrl('x'))) {
const selected = this.list.selected();
if (selected !== undefined && this.onDeleteRequest !== undefined) {
this.deleteState = { session: selected, phase: 'confirm' };
this.invalidate();
}
return;
}
if (matchesKey(data, Key.escape)) {
if (this.list.clearQuery()) {
this.visibleCount = Math.min(this.filteredSessions().length, this.pageSize);
Expand All @@ -231,7 +253,16 @@ export class SessionPickerComponent extends Container implements Focusable {
}
if (matchesKey(data, Key.enter)) {
const session = this.list.selected();
if (session) this.onSelect(session);
if (session) {
const selection = this.onSelect(session);
if (selection !== undefined) {
this.selectInFlight = true;
const clear = (): void => {
this.selectInFlight = false;
};
void selection.then(clear, clear);
}
}
return;
}

Expand All @@ -241,6 +272,52 @@ export class SessionPickerComponent extends Container implements Focusable {
}
}

private handleDeleteInput(data: string): void {
const state = this.deleteState;
if (state === undefined || state.phase === 'deleting') return;
const k = printableChar(data);
if (matchesKey(data, Key.escape) || k === 'n' || k === 'N') {
this.deleteState = undefined;
this.invalidate();
return;
}
if (k === 'y' || k === 'Y') {
this.deleteState = { session: state.session, phase: 'deleting' };
this.invalidate();
const sessionId = state.session.id;
const clear = (): void => {
if (this.deleteState?.session.id !== sessionId) return;
this.deleteState = undefined;
this.invalidate();
};
// then(clear, clear): rejections settle too — the host has already surfaced the failure.
void this.onDeleteRequest?.(state.session).then(clear, clear);
}
}

private renderDeleteStateLine(width: number): string {
const state = this.deleteState;
if (state === undefined) return '';
const rawTitle = (state.session.title ?? state.session.id).trim() || state.session.id;
const label = singleLine(rawTitle);
const prefix = state.phase === 'confirm' ? 'Delete session "' : 'Deleting session "';
const suffix = state.phase === 'confirm' ? '"? [y/N]' : '"…';
const labelBudget = Math.max(0, width - visibleWidth(prefix) - visibleWidth(suffix));
const shown = truncateToWidth(label, labelBudget, ELLIPSIS);
// The suffix carries the confirm/cancel keys: it survives by truncating
// the head (prefix + label) instead of the composed line.
const head = truncateToWidth(
prefix + shown,
Math.max(0, width - visibleWidth(suffix)),
ELLIPSIS,
);
const styled =
state.phase === 'confirm'
? currentTheme.boldFg('warning', head + suffix)
: currentTheme.fg('textMuted', head + suffix);
return truncateToWidth(styled, width, ELLIPSIS);
}

override render(width: number): string[] {
return this.renderLines(width).map((line) => truncateToWidth(line, width, ELLIPSIS));
}
Expand Down Expand Up @@ -293,6 +370,7 @@ export class SessionPickerComponent extends Container implements Focusable {
...(view.query.length > 0 ? ['Backspace clear'] : []),
'↑↓ navigate',
scopeHint,
...(this.onDeleteRequest !== undefined ? ['Ctrl+X delete'] : []),
'Enter select',
'Esc cancel',
].filter((item): item is string => item !== undefined);
Expand Down Expand Up @@ -360,6 +438,11 @@ export class SessionPickerComponent extends Container implements Focusable {
lines.push(currentTheme.fg('textMuted', truncateToWidth(footer, width, ELLIPSIS)));
}

if (this.deleteState !== undefined) {
lines.push('');
lines.push(this.renderDeleteStateLine(width));
}

lines.push(currentTheme.fg('primary', '─'.repeat(width)));
return lines;
}
Expand Down
Loading
Loading