Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4982f65
fix(geometry): the print check sizes each body by itself and lays a l…
ManuelStaggl Nov 12, 2026
1650c35
fix(geometry): the print check looks at what lies on the bed body by …
ManuelStaggl Nov 12, 2026
a187d19
fix(geometry): the print check measures floors in layers and walls ac…
ManuelStaggl Nov 12, 2026
73b24bf
fix(geometry): a ceiling or a wide bridge stays an overhang though it…
ManuelStaggl Nov 12, 2026
79fcd85
fix(geometry): the print check blocks a body that is an open shell
ManuelStaggl Nov 12, 2026
ad4a9b3
fix(geometry): a triangle drawn to a line does not make a closed body…
ManuelStaggl Nov 12, 2026
b67d3e3
fix(geometry): a bridge ends on standing material on both sides, with…
ManuelStaggl Nov 12, 2026
e294d82
fix(geometry): the print check drops no body, no floor and no finding…
ManuelStaggl Nov 12, 2026
dd54fce
Merge remote-tracking branch 'origin/main' into fix/assistant-check-r…
ManuelStaggl Nov 12, 2026
b6662d5
fix(desktop): the print check of a change comes back with the tool ca…
ManuelStaggl Nov 12, 2026
9749dcf
fix(desktop): a clean print check names what it looked at and what it…
ManuelStaggl Nov 12, 2026
afaf18d
fix(desktop): a block is built only where the part has the plastic it…
ManuelStaggl Nov 12, 2026
c13327e
feat(desktop): the assistant designs for material and print time and …
ManuelStaggl Nov 12, 2026
49d038d
Merge remote-tracking branch 'origin/main' into fix/assistant-check-r…
ManuelStaggl Nov 12, 2026
b159c12
fix(desktop): the check after a change comes at once, once, and only …
ManuelStaggl Nov 12, 2026
1409f32
fix(desktop): ribs are as thick as the wall, never under two lines
ManuelStaggl Nov 12, 2026
4f4663d
fix(desktop): insert seats and wood screw holes are measured too, and…
ManuelStaggl Nov 12, 2026
6b33dba
fix(geometry): a bridge is found from its anchors, whichever way the …
ManuelStaggl Nov 12, 2026
89274a3
fix(desktop): the report of the print check fits its contract without…
ManuelStaggl Nov 12, 2026
d19ec0a
fix(desktop): a block is taken back by what the design measures, each…
ManuelStaggl Nov 12, 2026
0a3f08c
fix(desktop): the check still comes past 200 undo steps and never end…
ManuelStaggl Nov 12, 2026
ee59e9f
test(geometry): the cases of the three reviews of the print check, tu…
ManuelStaggl Nov 12, 2026
1546201
test(geometry): the run time of the overhang check is held by its gro…
ManuelStaggl Nov 12, 2026
560cd45
test(desktop): the text pick test sets the top view again before each…
ManuelStaggl Nov 12, 2026
baa7c5b
Merge branch 'fix/print-check-audit' into fix/assistant-check-rules
ManuelStaggl Nov 12, 2026
20ec2f7
Merge remote-tracking branch 'origin/main' into fix/assistant-check-r…
ManuelStaggl Nov 12, 2026
89b9847
Merge remote-tracking branch 'origin/test/text-pick-focus' into fix/a…
ManuelStaggl Nov 12, 2026
8df84a8
test(desktop): the joint tests read the revision once no trial is reb…
ManuelStaggl Nov 12, 2026
e0f821e
test(desktop): contract tests wait for the turn's end and read the wh…
ManuelStaggl Nov 12, 2026
332d879
Merge remote-tracking branch 'origin/test/text-pick-focus' into fix/a…
ManuelStaggl Nov 12, 2026
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
2 changes: 1 addition & 1 deletion apps/desktop/e2e/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1407,7 +1407,7 @@ test('the part material is chosen in the status bar and the profiles are edited
await supportFree.press('Space');
await expect(supportFree).not.toBeChecked();
// The design rule groups (2026-11-07) are on from the start; each turns off on its own.
for (const rule of ['rounding', 'printInPlace', 'details', 'snap']) await expect(assistant.getByTestId(`design-${rule}`).getByRole('switch')).toBeChecked();
for (const rule of ['rounding', 'printInPlace', 'details', 'snap', 'economy']) await expect(assistant.getByTestId(`design-${rule}`).getByRole('switch')).toBeChecked();
const rounding = assistant.getByTestId('design-rounding').getByRole('switch', { name: 'Fillets and chamfers' });
await rounding.press('Space');
await expect(rounding).not.toBeChecked();
Expand Down
55 changes: 51 additions & 4 deletions apps/desktop/e2e/solidcraft.contract.spec.ts

Large diffs are not rendered by default.

259 changes: 244 additions & 15 deletions apps/desktop/src/main/agents/blockTool.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion apps/desktop/src/main/agents/designTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const designTool: UpstreamTool = {
name: DESIGN_TOOL,
description:
'Record that the user turned a group of the design rules off or back on for this project, e.g. "no fillets" → ' +
'rounding false, "screw the hinge instead" → printInPlace false. Only for what the user said; leave out the groups ' +
'rounding false, "screw the hinge instead" → printInPlace false, "make it solid" → economy false. Only for what the user said; leave out the groups ' +
'they did not mention.',
inputSchema: {
type: 'object',
Expand All @@ -25,6 +25,7 @@ const designTool: UpstreamTool = {
printInPlace: choice('Print-in-place hinges, joints and clips'),
details: choice('Refined details (hole chamfers, sunk screw heads, lead-ins, even walls)'),
snap: choice('Sizes snapped to nozzle and layer height'),
economy: choice('Saving material and print time (shells and ribs instead of solid blocks, low build height, no needless support)'),
},
additionalProperties: false,
},
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/main/agents/viewContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,6 @@ export function withViewTool(
export const VIEW_GUIDANCE =
'A message may start with a [PartBuddy: …] note naming the face, edge or vertex the user selected in PartBuddy’s view; ' +
`that is what "this face", "this edge" or "the marked one" means. Call ${VIEW_TOOL} to see what the user looks at. ` +
`PartBuddy checks the part for printing after every change and shows the result to the user as a light; ${VIEW_TOOL} ` +
'gives you the same findings. Read them after your last change, before you call the part done. ' +
'PartBuddy checks the part for printing after every change and shows the result to the user as a light; the findings come ' +
`behind the answer of each tool call that changed the design, and ${VIEW_TOOL} gives the latest ones. ` +
'Take a screenshot only when the look of the part matters: it costs many tokens.';
29 changes: 25 additions & 4 deletions apps/desktop/src/main/agents/workLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ export interface WorkLogOptions {
/** Runs Undo or Redo `count` times as one request. */
travel(command: 'edit.undo' | 'edit.redo', count: number): Promise<void>;
emit(event: AgentEvent): void;
/** The answer of a call that moved the design's revision, as the model gets it (the print check goes behind it). */
changed?(result: ToolResult, after: DesignHistory): Promise<ToolResult>;
log?(line: string): void;
}

/** Tools that may change the design; every other tool only reads and is not tracked. */
export const CHANGING_TOOLS = new Set(['execute', 'batch', 'set_parameter', 'undo', 'redo', 'partbuddy_mechanism', 'partbuddy_block']);
export const CHANGING_TOOLS = new Set(['execute', 'batch', 'set_parameter', 'undo', 'redo', 'partbuddy_mechanism', 'partbuddy_block', 'partbuddy_place_drawing', 'partbuddy_move_drawing']);
/** Tools that move through the history instead of adding to it. */
const HISTORY_TOOLS = new Set(['undo', 'redo']);

Expand Down Expand Up @@ -62,9 +65,9 @@ export class WorkLog {
return {
instructions: () => upstream.instructions(),
listTools: () => upstream.listTools(),
callTool: (name, args, callId) => {
callTool: async (name, args, callId) => {
if (!CHANGING_TOOLS.has(name)) return upstream.callTool(name, args, callId);
return this.serial(async () => {
const { result, moved } = await this.serial(async () => {
const before = await this.readHistory();
const result: ToolResult = await upstream.callTool(name, args, callId);
const after = await this.readHistory();
Expand All @@ -73,8 +76,12 @@ export class WorkLog {
} else {
this.publish();
}
return result;
// Without the history nobody knows whether the design changed: said in the log, since the model then gets no check.
if (!before || !after) this.options.log?.(`the design's history could not be read round ${name}; its answer carries no print check`);
return { result, moved: before && after && designChanged(before, after) ? after : null };
});
// Outside the queue: the user's own changes do not wait for the check.
return moved && this.options.changed ? this.options.changed(result, moved) : result;
},
};
}
Expand Down Expand Up @@ -207,6 +214,20 @@ export class WorkLog {
}
}

/**
* True when the call left another design behind: the undo depth moved, or a change took the place of what Redo held.
* The revision alone says nothing: a trial, a save and a block that was taken back move it and leave the design as it was.
*/
export function designChanged(before: DesignHistory, after: DesignHistory): boolean {
if (before.documentId !== after.documentId || before.undo !== after.undo || (before.redo > 0 && after.redo === 0)) return true;
// At the cap a step adds to the history and its depth stays: there a moved revision is all that tells, and it
// counts, at the price of a check too many after a trial. Without this no change past the cap got its check.
return after.undo >= UNDO_CAP && before.revision !== after.revision;
}

/** How deep SolidCraft's undo history goes (0.40.1, measured 2026-11-12: 260 steps read as 200). */
export const UNDO_CAP = 200;

/** True when the history grew or a new change replaced what Redo could have brought back. */
export function addedToHistory(before: DesignHistory, after: DesignHistory): boolean {
if (before.documentId !== after.documentId || before.revision === after.revision) return false;
Expand Down
16 changes: 11 additions & 5 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { readVersion } from './solidcraft/engineVersion';
import { safeDesignFileName } from './solidcraft/headless';
import { longPath, SolidCraftSupervisor } from './solidcraft/supervisor';
import { ChatArchive } from './chat/chatArchive';
import { printCheckNote } from './print/printCheckNote';
import { CHECK_PENDING_VIEW, CheckTold, printCheckNote, withCheckOfChange } from './print/printCheckNote';
import { templatePrintFaces, templateSteps } from '../shared/templates';
import { testPieceSteps } from '../shared/testPieces';
import { sketchNote } from './agents/sketchNote';
Expand Down Expand Up @@ -371,11 +371,13 @@ if (!app.requestSingleInstanceLock()) {
// The check as partbuddy_view shows it: a check of an older revision is still catching up.
const checkForView = () => {
if (!printCheck) return null;
if (printCheck.revision !== supervisor.currentRevision()?.revision)
return '[PartBuddy: the ready-to-print check of the latest change is still running; call partbuddy_view again in a moment.]';
return printCheckNote(printCheck);
if (printCheck.revision !== supervisor.currentRevision()?.revision) return CHECK_PENDING_VIEW;
return printCheckNote(printCheck, true);
};
// What the tool answers of this turn already said of the check: the same verdict is not told in full again.
const checkTold = new CheckTold();
bridge.handle(invokeChannels.printReportCheck, (report) => {
if (report.problem) log(`print check of revision ${String(report.revision)}: ${report.problem}`);
printCheck = report;
return undefined;
});
Expand Down Expand Up @@ -550,6 +552,9 @@ if (!app.requestSingleInstanceLock()) {
history: () => supervisor.history(),
travel: (command, count) => supervisor.travel(command, count),
emit: emitEvent,
// The check of each change goes back with the tool's answer, so no turn ends on a part the model never saw checked.
changed: (result, after) => withCheckOfChange(result, after, () => ({ mirrored: supervisor.currentRevision(), report: printCheck }), (report) => checkTold.note(report)),
log,
});
workLog = steps;
// Questions and plans as cards (M2a); changes wait until the user answered.
Expand Down Expand Up @@ -585,7 +590,7 @@ if (!app.requestSingleInstanceLock()) {
steps.tools(
withDesignTool(
withAssemblyTool(
withViewTool(guardNotes(withStandardTool(withPrintFaces(withBlockTool(withMechanismTool(withDrawings(upstream, drawings), () => printProfiles.part().printer.nozzle, keepMechanismFace), keepMechanismFace), keepMechanismFace)), (trial) => supervisor.trialSteps(trial)), () => viewState, () => notesService.forView(), checkForView),
withViewTool(guardNotes(withStandardTool(withPrintFaces(withBlockTool(withMechanismTool(withDrawings(upstream, drawings), () => printProfiles.part().printer.nozzle, keepMechanismFace), keepMechanismFace, log), keepMechanismFace)), (trial) => supervisor.trialSteps(trial)), () => viewState, () => notesService.forView(), checkForView),
() => viewState?.bodies.map((body) => body.name) ?? [],
saveAssembly,
),
Expand Down Expand Up @@ -729,6 +734,7 @@ if (!app.requestSingleInstanceLock()) {
});
const conversation = agent.conversation();
const sentAt = new Date();
checkTold.forget();
const lines = [
// Once after a restart: what the earlier conversation about this design was about.
chats?.takeDigest() ?? null,
Expand Down
Loading